使用内联汇编 (三) 内联汇编结束篇 - zengl开源网
三月 04, 2024 [C/C++] #obsidian #开源许可 #编译链接使用内联汇编 (三) 内联汇编结束篇 - zengl开源网
原文链接:https://www.zengl.com/a/201404/147.html
本文由zengl.com站长对 http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
另外再附加一个英特尔英文手册的共享链接地址:
http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)
本篇翻译对应汇编教程英文原著的第410页到第419页,目前翻译完前13章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为419 / 577)。
Using memory locations 使用内存位置:
之前的扩展asm格式里,介绍了如何通过寄存器来访问C程式里的变量,其实还可以直接使用变量的内存位置来访问变量的值,例如下面的memtest.c程式:
/* memtest.c - An example of using memory locations as values */
#include <stdio.h>
int main()
{
int dividend = 20;
int divisor = 5;
int result;
asm("divb %2\n\t"
"movl %1, %0"
: "=m"(result)
: "a"(dividend), "m"(divisor));
printf("The result is %d\n", result);
return 0;
}
上面代码里,result输出操作数和divisor输入操作数都用到了"m"约束符,在上一篇文章里,介绍过"m"约束符指的是"**使用变量的内存位置**",我们可以通过gcc -S来查看该约束符对应生成的汇编指令:
$ gcc -o memtest memtest.c $ ./memtest The result is 4 $ gcc -S memtest.c $ cat memtest.s ............................ ............................ movl $20, 28(%esp) movl $5, 24(%esp) movl 28(%esp), %eax
APP
9 "memtest.c" 1
divb 24(%esp) movl %eax, 20(%esp)
0 "" 2
#NO\_APP
movl 20(%esp), %edx
movl \$.LC0, %eax
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
............................ ............................ $
从上面的输出可以看到,内联汇编里第一条“divb %2”的指令变为了“divb 24(%esp)”,由于%2对应的输入操作数divisor使用的是"m"约束,所以%2占位符就被替换为了divisor变量对应的内存位置,由于该变量是一个局部变量,所以他的内存位置为esp栈顶指针加上一个偏移值(这里的偏移值为24)。同理第二条"movl %1, %0"的指令变为了"movl %eax, 20(%esp)",%1对应的dividend操作数使用的是"a"约束,所以%1就被替换为了%eax,%0对应的result操作数使用的是"=m"约束,所以%0被替换为了result变量的内存位置即20(%esp)。
注意:由于第一条内联汇编指令使用的是divb指令,该指令的后缀为字符b,即除数只有8位大小(除数的值必须小于256),被除数隐式存储在AX寄存器里,即被除数只有16位大小(被除数的值必须小于65536)。如果你想使用更大的除数或被除数,可以将divb指令调整为divw或divl指令。
Using floating-point values 使用浮点值:
由于FPU浮点寄存器是以寄存器栈的方式进行工作的(在之前的"**高级数学运算 (一) FPU寄存器介绍**"的章节里讲解过),所以,在内联汇编里使用FPU寄存器的方式和普通寄存器的使用方式有所不同,下面就介绍下,如何在内联汇编里使用FPU寄存器进行浮点运算。
在上一篇的约束符列表里,你可能已经注意到了三个和FPU寄存器相关的约束符:
-
"f"约束符:使用任何可用的浮点寄存器
-
"t"约束符:使用第一个ST0浮点寄存器
-
"u"约束符:使用第二个ST1浮点寄存器
当你从FPU里获取结果时,不能使用"f"约束符,必须使用"t"或"u"约束符来明确的指定结果所在的寄存器,例如下面的代码片段:
asm( “fsincos”
: “=t”(cosine), “=u”(sine) : “0”(radian));
FSINCOS指令会根据ST0里的弧度值,计算出对应的cosine(余弦值)和sine(正弦值),并将计算的正弦值先压入栈,再将计算的余弦值后压入栈,这样正弦值就由ST0移到ST1里了,而后压入栈的余弦值就位于ST0里,所以上面的代码片段里,第一个cosine输出操作数使用的是"=t"的约束符(表示将ST0里的余弦值输出到cosine变量里),第二个sine输出操作数使用的是"=u"约束符(表示将ST1里的正弦值存储到sine变量里)。另外,radian(弧度值)使用的是"0"占位符,表示使用和cosine一样的ST0寄存器来存储计算用的弧度值。
上面只是代码片段,完整的例子如下所示:
/* sincostest.c - An example of using two FPU registers */
#include <stdio.h>
int main()
{
float angle = 90;
float radian, cosine, sine;
radian = angle / 180 * 3.14159;
asm("fsincos"
:"=t"(cosine), "=u"(sine)
:"0"(radian));
printf("The cosine is %f, and the sine is %f\n", cosine, sine);
return 0;
}
上面代码的执行情况如下所示:
$ gcc -o sincostest sincostest.c $ ./sincostest The cosine is 0.000001, and the sine is 1.000000 $
可以看到,90度的余弦值和正弦值被成功存储到cosine和sine对应的变量里了,另外,使用gcc -S查看到的汇编指令如下:
$ gcc -S sincostest.c $ cat sincostest.s ............................ ............................ flds 40(%esp)
APP
9 "sincostest.c" 1
fsincos
0 "" 2
#NO\_APP
............................
fstps 52(%esp) ............................ fstps 48(%esp) ............................ ............................ $
汇编指令会先用flds指令将弧度值压入ST0,在fsincos计算出结果后,再通过fstps指令将寄存器栈里的余弦值和正弦值依次弹出并存储到cosine与sine的内存位置里。
另外,如果在内联汇编的代码里使用了某个浮点寄存器,但是在输入和输出的操作数里并没有指定该寄存器对应的约束符的话,也要将该浮点寄存器写入到被修改的寄存器列表里,如下面的areatest.c程式:
/* areatest.c - An example of using floating point regs */
#include <stdio.h>
int main()
{
int radius = 10;
float area;
asm("fild %1\n\t"
"fimul %1\n\t"
"fldpi\n\t"
"fmul %%st(1), %%st(0)"
: "=t"(area)
:"m"(radius)
: "%st(1)");
printf("The result is %f\n", area);
return 0;
}
上面汇编代码的作用是计算radius半径对应的圆面积,它会先用fild指令将radius半径值压入ST0,再通过fimul指令计算出半径的平方,接着通过fldpi指令加载3.14159...的pi值到ST0,这样原来ST0里的半径平方值就会自动移入ST1,最后通过fmul %%st(1), %%st(0)指令将ST1里的半径平方和ST0里的PI值相乘,得到的面积值存储在ST0里,最后ST0里的值会输出到area变量里,由于内联汇编里使用到了ST1寄存器,但是在输入和输出操作数列表里只指定了一个"=t"的约束符,该约束符表示ST0寄存器,因此,操作数列表里并没有指定使用ST1寄存器,所以需要在最后的被修改的寄存器列表里加入"%st(1)",这样编译器才会对该寄存器做一些善后工作。
Handling jumps 在内联汇编里使用跳转指令:
在内联汇编里可以自定义标签名,并且可以使用跳转指令来跳转到这些自定义的标签处。
下面的jmptest.c程式就演示了如何自定义标签名以及跳转指令的使用:
/* jmptest.c - An example of using jumps in inline assembly */
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int result;
asm("cmp %1, %2\n\t"
"jge greater\n\t"
"movl %1, %0\n\t"
"jmp end\n"
"greater:\n\t"
"movl %2, %0\n"
"end:"
:"=r"(result)
:"r"(a), "r"(b));
printf("The larger value is %d\n", result);
return 0;
}
上面的内联汇编代码里,定义了两个标签:greater和end标签,当第一条cmp指令的比较结果是%2对应的目标操作数大于等于%1对应的源操作数时,紧随其后的jge greater指令就会跳转到greater标签处,将较大的%2对应的操作数作为结果进行输出。反之,如果cmp指令比较的结果是%2对应的目标操作数小于%1对应的源操作数时,则jge指令就不会跳转,而是继续往下执行"movl %1, %0"的指令,从而将较大的%1操作数作为结果,最后通过"jmp end"指令跳转到end结束标签处。
使用gcc -S生成的汇编指令里也包含了对应的标签名:
$ gcc -S jmptest.c
$ cat jmptest.s
...............................
...............................
#APP
# 9 "jmptest.c" 1
cmp %eax, %edx
jge greater
movl %eax, %ebx
jmp end
greater:
movl %edx, %ebx
end:
...............................
...............................
$
从上面的输出可以看到,目标汇编指令里也包含了greater和end标签名。
此外,虽然可以在内联汇编里自定义标签名,但是这么做也存在两方面的限制,第一个限制是:内联汇编里的jmp之类的跳转指令,只能在同一个asm代码段里进行跳转,不能跳转到其他的asm代码段里定义的标签处。
第二个限制是:由于这些标签名会被编译进最终的汇编指令里,这样在不同的asm代码段里就不能定义相同的标签名,否则编译器就会报相同标签名的错误,另外,这些自定义的标签名还不能与C程式里的函数名,全局变量等的名字相同,否则编译器也会报错。
要解决相同标签名的问题,最好的方法就是使用局部标签,局部标签名就是使用数字(0到99的范围)作为标签名,局部标签名可以被重复定义很多次,相同的数字也可以在同一个代码段里被定义多次,如下面的jmptest2.c程式:
/* jmptest2.c - An example of using generic jumps in inline assembly */
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int result;
asm("cmp %1, %2\n\t"
"jge 0f\n\t"
"movl %1, %0\n\t"
"jmp 1f\n "
"0:\n\t"
"movl %2, %0\n "
"1:"
:"=r"(result)
:"r"(a), "r"(b));
printf("The larger value is %d\n", result);
return 0;
}
jmptest2.c使用0和1的局部标签名来代替之前的greater和end标签,在使用jge与jmp指令跳转时,数字标签名后面跟了一个字符,例如jge 0f,0后面的"f"是forward向前跳转的意思,这样jge就会向前跳转到最接近的0标签处,同理jmp 1f指令就是向前跳转到最接近的1标签处,如果想向后跳转,就必须使用"b"字符,来表示backward向后跳转。
Using Inline Assembly Code 使用内联汇编代码:
有的时候,具有相同功能的一段内联汇编代码其实可以写成宏的形式,宏会被编译器自动展开为完整的汇编代码,下面先介绍C语言里宏的概念,再介绍如何在C程式里为内联汇编创建宏。
What are macros? 什么是宏:
在C和C++的程式里,最简单的宏的定义格式如下:
define NAME expression
通过#define关键字来定义宏,NAME为宏名称(习惯上,宏名称都是大写字母,这样不容易和函数名发生冲突),expression为表达式,表达式可以是数字或字符串类型的常量值。
当定义了一个NAME宏时,C程式里所有出现NAME的地方都会被编译器替换为对应的expression表达式,例如,假设在一开始定义了一个如下所示的宏:
define MAX_VALUE 1024
上面定义了一个名为MAX\_VALUE的宏,对应的宏值为1024,那么如果在C程式里有一段下面的代码:
data = MAX\_VALUE;
if (size > MAX\_VALUE)
编译器在编译时,就会将上面的MAX\_VALUE宏替换为1024,所以上面的代码等效于:
data = 1024
if (size > 1024)
宏可以放置的位置是由宏值来决定的,例如,上面的MAX\_VALUE对应的值为1024,那么MAX\_VALUE就只能参与数值计算,不能像变量那样被赋值,如:MAX\_VALUE = 123这样的式子是不对的,因为编译器替换后,该式子就会变为:1024 = 123,数字常量本身是不能被修改和赋值的,所以放置宏时,一定要根据宏值来决定。
宏除了可以用来替换普通的数字,字符串等简单的常量表达式外,还有一种宏函数的格式,下面就对这种格式进行介绍。
C macro functions C语言宏函数:
宏函数的格式如下:
define NAME(input values, output value) (function)
宏函数也是用#define关键字来定义的,NAME为宏函数的名称,接着括号里的(input values, output value)是宏函数的参数列表部分,function为具体的一段需要替换的表达式,参数列表部分,既可以包含input values输入参数,也可以包含output value输出参数。宏函数也是在一开始定义好后,就可以在C程式的任意地方进行使用,编译器会自动完成相关的替换工作。
宏函数的例子可以参考下面的mactest1.c程式:
/* mactest1.c - An example of a C macro function */
#include <stdio.h>
#define SUM(a, b, result) \
((result) = (a) + (b))
int main()
{
int data1 = 5, data2 = 10;
int result;
float fdata1 = 5.0, fdata2 = 10.0;
float fresult;
SUM(data1, data2, result);
printf("The result is %d\n", result);
SUM(1, 1, result);
printf("The result is %d\n", result);
SUM(fdata1, fdata2, fresult);
printf("The floating result is %f\n", fresult);
SUM(fdata1, fdata2, result);
printf("The mixed result is %d\n", result);
return 0;
}
上面代码里定义了一个名为SUM的宏函数,参数列表里有三个参数,然后就是一段要被替换的表达式,我们可以使用gcc -E来查看宏展开后的代码:
$ gcc -E mactest1.c
.......................
.......................
int main()
{
int data1 = 5, data2 = 10;
int result;
float fdata1 = 5.0, fdata2 = 10.0;
float fresult;
((result) = (data1) + (data2));
printf("The result is %d\n", result);
((result) = (1) + (1));
printf("The result is %d\n", result);
((fresult) = (fdata1) + (fdata2));
printf("The floating result is %f\n", fresult);
((result) = (fdata1) + (fdata2));
printf("The mixed result is %d\n", result);
return 0;
}
$
从输出可以看到,源代码里的"SUM(data1, data2, result)"被替换为了"((result) = (data1) + (data2))","SUM(1, 1, result)"被替换为了"((result) = (1) + (1))",“SUM(fdata1, fdata2, fresult)”则被替换为了"((fresult) = (fdata1) + (fdata2))" ,所以宏函数只是根据提供的实际参数来进行简单的替换,然后对替换后的代码进行编译,生成最终的汇编指令。
我们可以使用宏函数来定义和使用内联汇编,下面就对这种方式进行介绍:
Creating inline assembly macro functions 创建内联汇编的宏函数:
内联汇编的宏函数,在格式上和普通的C宏函数没什么太大的区别,都由宏名,宏参数,宏表达式组成,只不过宏表达式是asm定义的内联汇编代码而已,例如下面的mactest2.c程式:
/* mactest2.c - An example of using inline assembly macros in a program */
#include <stdio.h>
#define GREATER(a, b, result) ({ \
asm("cmp %1, %2\n\t" \
"jge 0f\n\t" \
"movl %1, %0\n\t" \
"jmp 1f\n\t" \
"0:\n\t" \
"movl %2, %0\n\t" \
"1:" \
:"=r"(result) \
:"r"(a), "r"(b)); })
int main()
{
int data1 = 10;
int data2 = 20;
int result;
GREATER(data1, data2, result);
printf("a = %d, b = %d result: %d\n", data1, data2, result);
data1 = 30;
GREATER(data1, data2, result);
printf("a = %d, b = %d result: %d\n", data1, data2, result);
return 0;
}
同样的,我们可以使用gcc -E来查看宏展开后的代码:
$ gcc -E mactest2.c
.........................
.........................
int main()
{
int data1 = 10;
int data2 = 20;
int result;
({ asm("cmp %1, %2\n\t" "jge 0f\n\t" "movl %1, %0\n\t" "jmp 1f\n\t" "0:\n\t" "movl %2, %0\n\t" "1:" :"=r"(result) :"r"(data1), "r"(data2)); });
printf("a = %d, b = %d result: %d\n", data1, data2, result);
data1 = 30;
({ asm("cmp %1, %2\n\t" "jge 0f\n\t" "movl %1, %0\n\t" "jmp 1f\n\t" "0:\n\t" "movl %2, %0\n\t" "1:" :"=r"(result) :"r"(data1), "r"(data2)); });
printf("a = %d, b = %d result: %d\n", data1, data2, result);
return 0;
}
$
可以看到GREATER宏函数都被替换为对应的asm内联汇编代码了,这样同一种功能的内联汇编代码就不需要重复进行编写,使用宏函数让代码也变得简洁,容易阅读和维护。
以上就是内联汇编的相关内容,剩下的是些英文原著的总结部分,限于篇幅,就不多说了。
下一篇开始介绍如何在C程式里调用汇编程式里定义的函数。
OK,就到这里,休息,休息一下 o(∩\_∩)o\~\~