(转)详解汇编系统调用过程(以printf为例) |
您所在的位置:网站首页 › 汇编invoke › (转)详解汇编系统调用过程(以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] view plaincopy .section .data format: .asciz "%d\n" .section .text .global _start _start: pushl $12 pushl $format call printf movl $0, (%esp) 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] view plaincopy Disassembly of section .text: 080481c0 : 80481c0: 6a 0c push $0xc 80481c2: 68 cc 92 04 08 push $0x80492cc 80481c7: e8 d4 ff ff ff call 80481a0 80481cc: c7 04 24 00 00 00 00 movl $0x0,(%esp) 80481d3: e8 d8 ff ff ff call 80481b0
下面使用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:7warning: Source file is more recent than executable. 7 pushl $12 (gdb) info reg //察看寄存器状况,这里只显示需要注意的寄存器esp 0xbffff870 0xbffff870eip 0x80481c0 0x80481c0 //指着第一条指令地址
(gdb) s //执行一步,eip指着下一条指令地址8 pushl $format(gdb) info regesp 0xbffff86c 0xbffff86c // 86c = 870 - 4,对比上一条的esp,小了4,也就是stack增长了4个字节eip 0x80481c2 0x80481c2
(gdb) s //执行一步,下一条就是printf系统调用9 call printf(gdb) info regesp 0xbffff868 0xbffff868 // 868 = 86c - 4,增长了4个字节eip 0x80481c7 0x80481c7
//////////重点来了 (gdb) s0xb7e91110 in printf () from /lib/libc.so.6 //执行一步,正式进入printf(gdb) info regesp 0xbffff864 0xbffff864 // 864 = 868 - c,新push进去4个字节eip 0xb7e91110 0xb7e91110 (gdb) x /1x $esp0xbffff864: 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:1010 movl $0, (%esp)(gdb) info regeax 0x3 3 // eax保存着这次printf的返回值,也就是被打印的字符数量,12\n,一共3个字符。esp 0xbffff868 0xbffff868 // esp恢复到call printf之前的状态eip 0x80481cc 0x80481cc //恢复eip
(gdb) s //执行movl指令,下一条是call exit11 call exiteax 0x3 3esp 0xbffff868 0xbffff868eip 0x80481d3 0x80481d3 (gdb) x /1x $esp 0xbffff868: 0x00000000 //esp并没有增长,因为printf之前的数据已经没用了,我没有把他们pop出来,而是直接用新的数据刷写esp所指的内存
(gdb) s0xb7e77c80 in exit () from /lib/libc.so.6(gdb) sSingle 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的思路,逻辑上应该是行得通的。) 希望对大家有用。
|
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |