linux c 中嵌入汇编
为了提高代码的执行效率,有时程序中需要使用汇编语言来编制源代码。这就涉及在两种语言的相互调用问题,而且linux 使用的是 AT&T 的汇编语言格式,与 INTEL 汇编的有所区别,详细可以参考相关书籍。
在汇编应用程序中调用 C 函数主要涉及汇编程序如何向 C 函数传递参数以及相关寄存器的保存,我们先看一个将C 程序转换编译成汇编程序的代码,看看汇编程序调用函数时所做的处理:
/* 交换 a 和 b 的值 */
void swap(int *a, int *b, int d, int e, int f, int g, int h, int m, int n)
{
int c;
c = *a; *a = *b; *b = c;
}
int main(void)
{
int a, b;
a = 16; b = 32;
swap(&a, &b, 0, 0, 0, 0, 0, 0, 0);
return (a - b);
}
将文件保存为 swap.c,这里将 swap.c 文件中加入了多余的几个参数,来说明汇编程序的传递参数机制。
使用命令 gcc -Wall -S -o swap.s swap.c 生成该 C 语言程序的汇编程序 swap.s
swap:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %eax
movl (%eax), %eax
movl %eax, -4(%ebp)
movl 12(%ebp), %eax
movl (%eax), %edx
movl 8(%ebp), %eax
movl %edx, (%eax)
movl 12(%ebp), %edx
movl -4(%ebp), %eax
movl %eax, (%edx)
leave
ret
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $52, %esp
movl $16, -8(%ebp)
movl $32, -12(%ebp)
movl $0, 32(%esp)
movl $0, 28(%esp)
movl $0, 24(%esp)
movl $0, 20(%esp)
movl $0, 16(%esp)
movl $0, 12(%esp)
movl $0, 8(%esp)
leal -12(%ebp), %eax
movl %eax, 4(%esp)
leal -8(%ebp), %eax
movl %eax, (%esp)
call swap
movl -8(%ebp), %edx
movl -12(%ebp), %eax
movl %edx, %ecx
subl %eax, %ecx
movl %ecx, %eax
addl $52, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
在 swap 中可以看出汇编程序从 main 的栈中取出参数 a 和 b,然后进行交换,并返回;而在 main 程序中,调用 swap 之前为它的参数分配了 52 字节的空间,显然我们这里只是使用了9个参数需要的空间为 4*9=36 字节的空间就够使用,那么额外的 52-36=16 字节用作什么用途呢?另外在 main 的开始处的 andl 指令我觉得应该是使栈空间16字节对齐的操作;当将 C 程序中 swap 函数的参数改变时,可以看出其对应的汇编程序中也总是有16字节的额外空间,留作疑问。
C 语言中嵌入汇编的操作,只需要遵循一定的格式,然后使用汇编指令就可以,如下面代码
char* strcpy(char *dest, const char *src)
{
__asm__
("cld/n"
"1:/tloadsb/n/t"
"stosb/n/t"
"testb %%al, %%al/n/t"
"jne 1b"
::"S"(src),"D"(dest)//:"si","di","ax"/* 可能是应用程序中不用指出变化的寄存器 */
/* 否则将 gcc 报错: can't find a register in class ‘SIREG’ while reloading ‘asm’ */
);
return dest;
}
上面的/n/t转换是为了汇编代码整齐,没有特殊的含义。可以看出,嵌入汇编时需要使用 __asm__(); 嵌入汇编代码,也可以使用 asm(); 但是为了向前兼容最好使用前面的格式。
而在C程序调用汇编函数过程中,只要遵循参数的反向压入栈,而且汇编程序按照依次取出参数就可以了,从上面的几个例子中不难写出一个C函数调用纯汇编过程的源程序,不再示例。
/*********************************************************************************************************************************************************************************/
ebp是基址寄存器,指向的是栈低,也就是高地址。而esp指向的是栈顶,相对ebp是低地址。当执行call时,就把call这条指令的地址压入堆栈,以备该call完成后退出所用。sub指令用于在堆栈中为call中的局部变量分配内存空间。
/*********************************************************************************************************************************************************************************/
esp必须要负责指向栈顶
pubshl %ebp是保存寄存器ebp,因为以后要用到ebp
movl %esp , %ebp是让ebp此时也指向esp此刻所指向的位置,因为随后esp需要改变,在堆栈中必须留出局部变量所需要的空间,指令subl $4 , %esp达到此目的.此时用ebp来访问实参和局部变量都是比较方便的,如果用esp的相对寄存器寻址方式来访问的话就比较不稳定,因为esp经常需要改变,那么那个相对数就要改变
/*********************************************************************************************************************************************************************************/
esp要时刻指向栈的顶部。
给你点参考吧:
概念:SFP (Stack Frame Pointer) 栈框架指针
正确理解SFP必须了解:
IA32 的栈的概念
CPU 中32位寄存器ESP/EBP的作用
PUSH/POP 指令是如何影响栈的
CALL/RET/LEAVE 等指令是如何影响栈的
如我们所知:
1)IA32的栈是用来存放临时数据,而且是LIFO,即后进先出的。栈的增长方向是从高地址向低地址增长,按字节为单位编址。
2) EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。
3) PUSH一个long型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。
4) POP一个long型数据,过程与PUSH相反,依次将ESP-4、ESP-3、ESP-2、ESP-1从栈内弹出,放入一个32位寄存器。
5) CALL指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。
6) RET指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行
7) ENTER是建立当前函数的栈框架,即相当于以下两条指令:
pushl %ebp
movl %esp,%ebp
8) LEAVE是释放当前函数或者过程的栈框架,即相当于以下两条指令:
movl ebp esp
popl ebp
如果反汇编一个函数,很多时候会在函数进入和返回处,发现有类似如下形式的汇编语句:
pushl %ebp ; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址
movl %esp,%ebp ; esp值赋给ebp,设置 main函数的栈基址
........... ; 以上两条指令相当于 enter 0,0
...........
leave ; 将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址
ret ; main函数返回,回到上级调用
这些语句就是用来创建和释放一个函数或者过程的栈框架的。
原来编译器会自动在函数入口和出口处插入创建和释放栈框架的语句。
函数被调用时:
1) EIP/EBP成为新函数栈的边界
函数被调用时,返回时的EIP首先被压入堆栈;创建栈框架时,上级函数栈的EBP被压入堆栈,与EIP一道行成新函数栈框架的边界
2) EBP成为栈框架指针SFP,用来指示新函数栈的边界
栈框架建立后,EBP指向的栈的内容就是上一级函数栈的EBP,可以想象,通过EBP就可以把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现 backtrace功能的
3) ESP总是作为栈指针指向栈顶,用来分配栈空间
栈分配空间给函数局部变量时的语句通常就是给ESP减去一个常数值,例如,分配一个整型数据就是 ESP-4
4) 函数的参数传递和局部变量访问可以通过SFP即EBP来实现
由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:
+8+xx(%ebp) ; 函数入口参数的的访问
-xx(%ebp) ; 函数局部变量访问
假如函数A调用函数B,函数B调用函数C ,则函数栈框架及调用关系如下图所示:
下图有点乱,因此删去部分内容,要看原图可参考我的blog
+----------------------------+----> 高地址
| EIP (上级函数返回地址) |
+----------------------------+
| EBP (上级函数的EBP) |
+----------------------------+
| Local Variables |
| .......... |
+-----------------------------+
| Arg n(函数B的第n个参数) |
+-----------------------------+
| Arg .(函数B的第.个参数) |
+-----------------------------+
| Arg 1(函数B的第1个参数) |
+-----------------------------+
| Arg 0(函数B的第0个参数) |
+-----------------------------+
EIP (A函数的返回地址) |
+-----------------------------+
| EBP (A函数的EBP) |
+-----------------------------+
| Local Variables |
| .......... |
+-----------------------------+
| Arg n(函数C的第n个参数) |
+-----------------------------+
| Arg .(函数C的第.个参数) |
+-----------------------------+
| Arg 1(函数C的第1个参数) |
+-----------------------------+
| Arg 0(函数C的第0个参数) |
+-----------------------------+
| EIP (B函数的返回地址) |
+-----------------------------+
| EBP (B函数的EBP) |
+-----------------------------+
| Local Variables |
| .......... |
+-----------------------------+---> 低地址
图 1-1
再分析test1反汇编结果中剩余部分语句的含义:
# mdb test1
Loading modules: [ libc.so.1 ]
> main::dis ; 反汇编main函数
main: pushl %ebp
main+1: movl %esp,%ebp ; 创建Stack Frame(栈框架)
main+3: subl $8,%esp ; 通过ESP-8来分配8字节堆栈空间
main+6: andl $0xf0,%esp ; 使栈地址16字节对齐
main+9: movl $0,%eax ; 无意义
main+0xe: subl %eax,%esp ; 无意义
main+0x10: movl $0,%eax ; 设置main函数返回值
main+0x15: leave ; 撤销Stack Frame(栈框架)
main+0x16: ret ; main 函数返回
>
以下两句似乎是没有意义的,果真是这样吗?
movl $0,%eax
subl %eax,%esp
用gcc的O2级优化来重新编译test1.c:
# gcc -O2 test1.c -o test1
# mdb test1
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: xorl %eax,%eax ; 设置main返回值,使用xorl异或指令来使eax为0
main+0xb: leave
main+0xc: ret
>
新的反汇编结果比最初的结果要简洁一些,果然之前被认为无用的语句被优化掉了,进一步验证了之前的猜测。
提示:编译器产生的某些语句可能在程序实际语义上没有用处,可以用优化选项去掉这些语句。
问题:为什么用xorl来设置eax的值?
注意到优化后的代码中,eax返回值的设置由 movl $0,%eax 变为 xorl %eax,%eax ,这是因为IA32指令中,xorl比movl有更高的运行速度。
概念:Stack aligned 栈对齐
那么,以下语句到底是和作用呢?
subl $8,%esp
andl $0xf0,%esp ; 通过andl使低4位为0,保证栈地址16字节对齐
表面来看,这条语句最直接的后果是使ESP的地址后4位为0,即16字节对齐,那么为什么这么做呢?
原来,IA32 系列CPU的一些指令分别在4、8、16字节对齐时会有更快的运行速度,因此gcc编译器为提高生成代码在IA32上的运行速度,默认对产生的代码进行16字节对齐
andl $0xf0,%esp 的意义很明显,那么 subl $8,%esp 呢,是必须的吗?
这里假设在进入main函数之前,栈是16字节对齐的话,那么,进入main函数后,EIP和EBP被压入堆栈后,栈地址最末4位二进制位必定是 1000,esp -8则恰好使后4位地址二进制位为0000。看来,这也是为保证栈16字节对齐的。
如果查一下gcc的手册,就会发现关于栈对齐的参数设置:
-mpreferred-stack-boundary=n ; 希望栈按照2的n次的字节边界对齐, n的取值范围是2-12
默认情况下,n是等于4的,也就是说,默认情况下,gcc是16字节对齐,以适应IA32大多数指令的要求。
/*********************************************************************************************************************************************************************************/
esp是栈顶指针。不同时刻,里面保存的数值是表示你栈顶的内存地址来的,是不一样的,两次调用同一个函数这个esp也可能是不一样的
一般的函数调用规范,就是把 esp 和 ebp 之间的一段内存区域作为局部变量用了,你也看到subl $20, %esp ; 这个了,就是保留这段区域出来的。
所以
-0x4(%ebp)
0x4(%esp)
这样都是引用那个区域的 局部变量。