(转)详解汇编系统调⽤过程(以printf为例)
本⽂以printf为例,详细解析⼀个简单的printf调⽤⾥头,系统究竟做了什么,各寄存器究竟如何变化。
环境:
linux + gnu as assembler + ld linker
如何在汇编调⽤glibc的函数?其实也很简单,根据c convention call的规则,参数反向压栈,call,然后结果保存在eax⾥头。注意,保存的是地址。
在汇编⾥头,⼀切皆地址。(别纠结这个,别告诉我还有⽴即数……主要是要有⼀切皆地址的思想)
例如这个printf,在C⾥头,我们⽤得很多
int printf(const char *format, ...) 这⾥值得⼀提的是这个“...”是不定参数,也就是说后⾯有多少个参数,函数定义⾥头没有规定,感兴趣的可以google⼀下va_list相关的知识,这⾥就不展开了。
但是汇编怎么知道处理这个的呢?这⾥给个简单的解释,感兴趣的可以google⼀下“c convention call”了解更详细跟专业的解释。
例如当我们调⽤ result = printf( "%d %d", 12, a )的时候,编译器默认是这样处理的(除⾮函数定义声明了pascal call)。
在栈⾥头,先⼀次push a的地址,还有12这个⽴即数,再push "%d %d"这个字符串的地址,内存模型如下,x86的esp是往下增长的。
(这⾥是buttom,往下增长的是top)
&a
12
address of "%d %d"
-------------------------------------------(esp 指着这⾥,我们假设地址是4字节,12这个数也是4字节)
当call printf的时候,⾸先,push当前的eip⼊esp,解析esp+4所指的"%d %d",因为%d这样的特定字符都定义了后⾯每个参数的⼤⼩,所以只要解析“%d %d”,我们就可以知道栈⾥头参数的情况,例如esp+4+4就是⼀个int,esp+4+4+4是另外⼀个int。
当返回的时候,先pop到eip,也就是把eip还原到call之后马上要执⾏的机器码,这时,esp就指着“%d %d”,esp+4指着12,esp+8指着a的地址。esp⾥头的内容怎么处理,看需要吧,你也可以pop出来,也可以不pop。但为了效率着想,如果空间够⽤,通常不pop,直接⽤mov 指令把下⼀次要⽤的参数move进去。返回指储存在eax⾥头。
这也⼀定程度上解释了为什么c convention call是反向压栈,这样编译器处理起来⽅便,特别对于这些va_list,因为va_list后⾯不能继续跟参数,va_list⼀定出现在函数的末尾,如果是对printf这类的函数使⽤pascal call,也就是参数正向压栈,汇编级别处理起来就特别⿇烦了。
眼见为实,下⾯就⽤汇编写⼀个调⽤printf的,并⽤gdb跟踪寄存器,看看是否是上述的⼀样。
⽂件:test_printf.s
[plain]
1. .section .data
2.        format: .asciz "%d\n"
3. .section .text
4. .global _start
5. _start:
6.        pushl $12
7.        pushl $format
8.        call printf
9.        movl $0, (%esp)
10.        call exit
使⽤如下命令编译,链接
$ as -g test_printf.s -o test_printf.o
$ ld -lc -I /lib/ld-linux.so.2 test_printf.o -o test_printf
as加⼊-g是要加⼊调试信息,ld的-lc是链接libc.a,-I是--dynamic-linker,/lib/ld-linux.so.2这个要看各⼈系统情况。链接libc跟ld库之后,⽣成test_printf
执⾏
$ ./test_printf
12
输出12,正常退出。
先⽤objdump看看test_printf⾥头的.text section
$ objdump -d test_printf
[plain]
1. Disassembly of section .text:
2.
3.
4. 080481c0 <_start>:
5.  80481c0:  6a 0c                  push  $0xc
6.  80481c2:  68 cc 92 04 08          push  $0x80492cc
7.  80481c7:  e8 d4 ff ff ff          call  80481a0 <printf@plt>
8.  80481cc:  c7 04 24 00 00 00 00    movl  $0x0,(%esp)
9.  80481d3:  e8 d8 ff ff ff          call  80481b0 <exit@plt>
下⾯使⽤gdb跟踪⼀下,看看上述是否正确。
$ gdb test_printf
(gdb) b _start  //设置断点到_start,主函数⼊⼝
Breakpoint 1 at 0x80481c0: file test_printf.s, line 7.
(gdb) run  //执⾏,遇到断点,停下,eip指着第7⾏,也就是第⼀条要执⾏的push指令
Starting program: /home/fengzh/research/c_and_asm/printf/test_printf
Breakpoint 1, _start () at test_printf.s:7
warning: Source file is more recent than executable.
7 pushl $12
(gdb) info reg  //察看寄存器状况,这⾥只显⽰需要注意的寄存器
esp            0xbffff870 0xbffff870
eip            0x80481c0 0x80481c0 <_start>  //指着第⼀条指令地址
(gdb) s  //执⾏⼀步,eip指着下⼀条指令地址
8 pushl $format
(gdb) info reg
esp            0xbffff86c 0xbffff86c  // 86c = 870 - 4,对⽐上⼀条的esp,⼩了4,也就是stack增长了4个字节
eip            0x80481c2 0x80481c2 <_start+2>
(gdb) s //执⾏⼀步,下⼀条就是printf系统调⽤
9 call printf
(gdb) info reg
esp            0xbffff868 0xbffff868  // 868 = 86c - 4,增长了4个字节
eip            0x80481c7 0x80481c7 <_start+7>
//////////重点来了
(gdb) s
0xb7e91110 in printf () from /lib/libc.so.6  //执⾏⼀步,正式进⼊printf
(gdb) info reg
esp            0xbffff864 0xbffff864  // 864 = 868 - c,新push进去4个字节
eip            0xb7e91110 0xb7e91110 <printf>
(gdb) x /1x $esp
0xbffff864: 0x080481cc  // esp的栈顶保存的是下⼀条要执⾏的代码的位置,movl的位置,(参考上⾯objdump的结果)
(gdb) s  //执⾏⼀步,printf已经执⾏完毕,
Single stepping until exit from function printf,
which has no line number information.
12  //这个是printf的输出
_start () at test_printf.s:10
10 movl $0, (%esp)
(gdb) info reg
eax            0x3                3  // eax保存着这次printf的返回值,也就是被打印的字符数量,12\n,⼀共3个字符。
esp            0xbffff868 0xbffff868  // esp恢复到call printf之前的状态exited
eip            0x80481cc 0x80481cc <_start+12>  //恢复eip
(gdb) s  //执⾏movl指令,下⼀条是call exit
11 call exit
eax            0x3 3
esp            0xbffff868 0xbffff868
eip            0x80481d3 0x80481d3 <_start+19>
(gdb) x /1x $esp
0xbffff868: 0x00000000  //esp并没有增长,因为printf之前的数据已经没⽤了,我没有把他们pop出来,⽽是直接⽤新的数据刷写esp所指的内存
(gdb) s
0xb7e77c80 in exit () from /lib/libc.so.6
(gdb) s
Single stepping until exit from function exit,
which has no line number information.
[Inferior 1 (process 1609) exited normally]
正常退出。⼀切都如上述。
经过这个简单的printf,我们可以清楚知道在⼀个glibc调⽤⾥头,汇编层⾯究竟是怎么做的,具体都做了些什么。
有了这个基础,如果各位想开发⼀门新语⾔,需要处理multiple return value的情况,就知道怎么做了。
例如,我需要处理这个函数[ a, b ] = function()
这个函数需要返回a跟b两个值。在c语⾔⾥头,构造⼀个struct,或者构造⼀个array,都是可⾏的。但是代码上看着就⽐较恶⼼,处理起来也⿇烦。c语⾔返回值就只有⼀个,所以⽤⼀个eax就⾜够了,要
么⼀个int,要么⼀个double,要么就⼀个地址,⽆论哪种情况,就1个寄存器就⾜够了(浮点型使⽤专门的st寄存器)
⽽如果是新的编译器需要处理这中语⾔,怎么做呢?在push参数之前,先push return value的address进去esp
例如
push a
push b
push parameter
在转跳函数⾥头,计算出参数a跟b的地址,之后把返回之存储到a跟b⾥头。就可以了。或者⽤eax,ebx之类的构造⼀个stack(这个我不⼤清楚是否可以,不过按照esp的思路,逻辑上应该是⾏得通的。)
希望对⼤家有⽤。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。