GCC 中的扩展内联汇编






4.83/5 (14投票s)
2003年10月28日
5分钟阅读

73701
本文描述了 gcc 中的扩展内联汇编
基本内联汇编
C 语言中内联汇编的格式非常简单
asm ("statements");
__asm__("statements")
也可以使用
asm 调用中的任何内容都将按原样放入 C 编译器生成的汇编输出中。然后,此汇编输出将被送入汇编器。这种内联汇编适用于执行 C 语言无法直接完成的任务,但我们不能将这些修改寄存器的指令放在 C 代码的中间。因为编译器只会将这段代码放入汇编输出中,它会在生成 C 代码的汇编时不知道你在操作寄存器。
但如果你使用此语句编写一个完整函数的代码,它会正常工作,因为你不会干扰编译器生成的汇编代码。
让我们看一个例子。考虑以下计算两个数组中数字的平均值并将结果存储在第三个数组中的小型程序。
int numX[5] = {10,20,30,40,50}; int numY[5] = {20,30,40,50,60}; int res[5]; void avg() { int i; for(i = 0; i < 5; ++i) { res[i] = (numX[i] + numY[i])/2; } } main() { int i; avg(); for(i =0; i <5 ; ++i) { printf("result[%d] = %d \n", i,res[i]); } }
现在让我们看看如何使用基本内联汇编将除以 2 的操作转换为汇编。
void avg() { int i; for(i = 0; i < 5; ++i) { res[i] = (numX[i] + numY[i]); asm(" movl -4(%ebp), %eax ; move i to eax movl _res(,%eax,4), %ebx ; move res[i] to ebx sarl %ebx ; divide ebx by two by shifting right movl %ebx, _res(,%ebx,4) ; move ebx to res[i] "); } }
当我们编译并运行此程序时,它运行正常,并打印出与上一个程序相同的结果。这里我们很幸运,我们正在改变 eax 和 ebx 寄存器的值,并且编译器在循环中没有使用它们来存储一些变量。
上面情况中 `avg` 函数的汇编代码如下。请注意,编译器将汇编语句放在 `asm()
` 函数内,就像它们在 `/APP` 和 `/NO_APP` 指令之间一样。
_avg:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $0, -4(%ebp) # i
L2:
cmpl $4, -4(%ebp) # i
jle L5
jmp L1
L5:
movl -4(%ebp), %ecx # i
movl -4(%ebp), %edx # i
movl -4(%ebp), %eax # i
movl _numY(,%eax,4), %eax # numY
addl _numX(,%edx,4), %eax # numX
movl %eax, _res(,%ecx,4) # res
/APP
movl -4(%ebp), %eax
movl _res(,%eax,4), %ebx
sarl %ebx
movl %ebx, _res(,%eax,4)
/NO_APP
leal -4(%ebp), %eax
incl (%eax) # i
jmp L2
L1:
leave
ret
这里我们可以看到 ebx 完全没有被使用,而 eax 在循环中每次都被加载为 i 的值。所以我们的汇编代码不会干扰编译器生成的代码。现在让我们尝试使用 -O2 优化来编译相同的程序。生成的汇编代码如下。
_avg:
pushl %ebp
xorl %edx, %edx
movl %esp, %ebp
movl $_numY, %ecx
pushl %esi
movl $_res, %esi
pushl %ebx
movl $_numX, %ebx
L6:
movl (%ecx,%edx,4), %eax # numY
addl (%ebx,%edx,4), %eax # numX
movl %eax, (%esi,%edx,4) # res
/APP
movl -4(%ebp), %eax
movl _res(,%eax,4), %ebx
sarl %ebx
movl %ebx, _res(,%eax,4)
/NO_APP
incl %edx # i
cmpl $4, %edx # i
jle L6
popl %ebx
popl %esi
popl %ebp
ret
这里编译器试图通过将许多东西移出循环并将变量的值保留在寄存器中来优化代码。这个程序将无法工作,并且会给出核心转储。原因是编译器使用 ebx 寄存器来保存 numX 的指针,而我们在内联汇编代码中更改了 ebx 的值。编译器不知道我们对 ebx 做了什么,仍然假设 ebx 将拥有 numX 的指针。
在 gcc 中,你可以使用扩展汇编来告诉编译器你在内联汇编代码中做了什么。例如,你弄乱了哪些寄存器。你甚至可以要求编译器为你将一些变量的值放入某些寄存器中。
扩展内联汇编
扩展内联汇编的语法与基本内联汇编相似,只是它允许指定输入寄存器、输出寄存器和被破坏的空间(寄存器和内存)。
语法如下
asm ( "statements" : output : input : clobbered);
- statements - 汇编语句
- output - 输入约束-名称对 `"constraint"` (name),用逗号分隔。
- input - 输出约束-名称对 `"constraint"` (name),用逗号分隔。
- clobbered - 被破坏寄存器的逗号分隔列表。如果你写入内存,则必须将 `"memory"` 作为被破坏值之一包含在内。这相当于破坏所有寄存器。这用于告知 gcc 我们可能更改了 gcc 认为在寄存器中的某些内存值。
输出和输入在汇编语句中使用 %0、%1 等数字进行引用。编号是根据它们出现的顺序进行的。首先为输出寄存器编号,然后为输入寄存器编号。
输入/输出的约束是:-
- g - 让编译器决定为变量使用哪个寄存器
- q - 加载到 eax、ebx、ecx、edx 中的任何可用寄存器
- r - 与 q 相同,但包括 esi 和 edi
- a - 加载到 eax 寄存器
- b - 加载到 ebx 寄存器
- c - 加载到 ecx 寄存器
- d - 加载到 edx 寄存器
- f - 加载到浮点寄存器
- D - 加载到 edi 寄存器
- S - 加载到 esi 寄存器
对于输出,约束前面加上 "="。寄存器也可以在汇编语句中直接访问,但在扩展汇编中,它们前面加上两个 % 而不是一个 %,例如 `%%eax`、`%%edx` 等。
让我们使用扩展内联汇编来完成相同的示例函数。
void avg() { int i; for(i = 0; i < 5; ++i) { res[i] = (numX[i] + numY[i]); asm("sarl %1 movl %1, %0": "=r"(res[i]) :"r" (res[i]), "memory"); } }
在这里,我们告诉编译器将 res[i] 加载到任何寄存器中,我们可以使用 %0 来引用该寄存器。此情况下的汇编代码如下。
_avg:
pushl %ebp
xorl %edx, %edx
movl %esp, %ebp
movl $_res, %ecx
pushl %esi
movl $_numX, %esi
pushl %ebx
movl $_numY, %ebx
L6:
movl (%ebx,%edx,4), %eax # numY
addl (%esi,%edx,4), %eax # numX
movl %eax, (%ecx,%edx,4) # res
/APP
sarl %eax
movl %eax, %eax
/NO_APP
movl %eax, (%ecx,%edx,4) # res
incl %edx # i
cmpl $4, %edx # i
jle L6
popl %ebx
popl %esi
popl %ebp
ret
让我们看一些更多的例子。在上面的例子中,输入和输出是相同的,所以我们可以使用约束 "0" 来告诉编译器,如下所示。
void avg() { int i; for(i = 0; i < 5; ++i) { res[i] = (numX[i] + numY[i]); asm("sarl %0 ": "=r"(res[i]) :"0" (res[i]), "memory"); } }
此情况下的汇编代码如下。
_avg:
pushl %ebp
xorl %edx, %edx
movl %esp, %ebp
movl $_res, %ecx
pushl %esi
movl $_numX, %esi
pushl %ebx
movl $_numY, %ebx
L6:
movl (%ebx,%edx,4), %eax # numY
addl (%esi,%edx,4), %eax # numX
movl %eax, (%ecx,%edx,4) # res
/APP
sarl %eax
/NO_APP
movl %eax, (%ecx,%edx,4) # res
incl %edx # i
cmpl $4, %edx # i
jle L6
popl %ebx
popl %esi
popl %ebp
ret
我们可以像这样在扩展汇编中编写加法部分。
void avg() { int i; for(i = 0; i < 5; ++i) { asm("movl %1, %0 addl %2, %0 " : "=r" (res[i]) :"r" (numX[i]), "r" (numY[i]): "memory" ); asm("sarl %0" : "=r"(res[i]) :"0" (res[i])); } }
这里我们将 `numX[i]` 加载到 %1,将 `numY[i]` 加载到 %2,并将输出即 `res[i]` 表示为 %0。此情况下的汇编代码如下。
_avg:
pushl %ebp
xorl %ecx, %ecx
movl %esp, %ebp
pushl %edi
pushl %esi
movl $_numX, %edi
pushl %ebx
movl $_numY, %esi
movl $_res, %ebx
L6:
movl (%edi,%ecx,4), %eax # numX
movl (%esi,%ecx,4), %edx # numY
/APP
movl %eax, %eax
addl %edx, %eax
/NO_APP
movl %eax, (%ebx,%ecx,4) # res
/APP
sarl %eax
/NO_APP
movl %eax, (%ebx,%ecx,4) # res
incl %ecx # i
cmpl $4, %ecx # i
jle L6
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
我们可以像这样将两个汇编语句合并为一个。
void avg() { int i; for(i = 0; i < 5; ++i) { asm("movl %1, %0 addl %2, %0 sarl %0 " : "=r" (res[i]) :"r" (numX[i]), "r" (numY[i]): "memory" ); } }
此情况下的汇编代码如下。
_avg:
pushl %ebp
xorl %ecx, %ecx
movl %esp, %ebp
pushl %edi
pushl %esi
movl $_numX, %edi
pushl %ebx
movl $_numY, %esi
movl $_res, %ebx
L6:
movl (%edi,%ecx,4), %eax # numX
movl (%esi,%ecx,4), %edx # numY
/APP
movl %eax, %eax
addl %edx, %eax
sarl %eax
/NO_APP
movl %eax, (%ebx,%ecx,4) # res
incl %ecx # i
cmpl $4, %ecx # i
jle L6
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
结论
使用 gcc 的扩展内联汇编,我们可以非常容易地编写内联汇编代码。它允许轻松访问局部和全局变量,因此你不必担心堆栈。你可以在 C 代码中的任何位置插入内联汇编代码,而不用担心会破坏编译器为 C 代码生成的汇编。
在我的下一篇文章中,我将讨论 MMX 指令以及如何使用扩展汇编为 MMX 指令构建易于使用的宏。