2.汇编语言系统调用过程

以printf为例,详细解析一个简单的printf调用里头,系统究竟做了什么,各寄存器究竟如何变化。

如何在汇编调用glibc的函数?其实也很简单,根据c convention call的规则,参数反向压栈,call,然后结果保存在eax里头。注意,保存的是地址。

在汇编里头,一切皆地址。

当我们调用 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

.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

-g是要加入调试信息

ld的-lc是链接libc.a,-I是--dynamic-linker,/lib/ld-linux.so.2

运行

#./test_printf

输出12

调试

用objdump看看test_printf里头的.text section,注意Disassembly of section .text

使用gdb跟踪,看看上述是否正确

#gdb test_printf

设置断点到_start

(gdb) break _start

(gdb) run

执行,遇到断点,停下,eip指着第6行,也就是第一条要执行的push指令

(gdb) info reg

察看寄存器状况

(gdb) s

执行一步,eip指着下一条指令地址

(gdb) info reg

esp 0xbffff6cc 0xbffff6cc

6cc = 6d0 - 4,对比上一条的esp,小了4,也就是stack增长了4个字节

(gdb) s

(gdb) info reg

esp 0xbffff6c8 0xbffff6c8

6c8 = 6cc - 4,对比上一条的esp,小了4,也就是stack增长了4个字节

(gdb) s

in printf () from /lib/libc.so.6

执行一步,正式进入printf

(gdb) info reg

esp 0xbffff6c4 0xbffff6c4

6c4=6c8-4 新push进去4个字节

(gdb) x /1x $esp
0xbffff6c4: 0x080481c4

esp的栈顶保存的是下一条要执行的代码的位置,movl的位置,(参考上面objdump的结果)

可以使用bt查看栈帧,下面对比栈变化

(gdb) s

printf出12,已经执行完毕

(gdb) info reg

eax保存着这次printf的返回值,也就是被打印的字符数量,12\n,一共3个字符。

esp恢复到call printf之前的状态

恢复eip

(gdb) s

执行movl指令,下一条是call exit

(gdb) x /1x $esp

esp并没有增长,因为printf之前的数据已经没用了,我没有把他们pop出来,而是直接用新的数据刷写esp所指的内存。

(gdb) s
(gdb) s

正常退出

关于EIP、ESP、EBP寄存器

1.EIP寄存器里存储的是CPU下次要执行的指令的地址。

也就是调用完fun函数后,让CPU知道应该执行main函数中的printf("函数调用结束")语句了。

2.EBP寄存器里存储的是是栈的栈底指针,通常叫栈基址这个是一开始进行fun()函数调用之前,由ESP传递给EBP的。(在函数调用前你可以这么理解:ESP存储的是栈顶地址,也是栈底地址。)

3.ESP寄存器里存储的是在调用函数fun()之后,栈的栈顶。并且始终指向栈顶。

堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。
允许插入或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称入栈和出栈。

有一组CPU指令可以实现对进程的内存实现堆栈访问。其中,POP指令实现出栈操作,PUSH指令实现入栈操作。
CPU的ESP寄存器存放当前线程的栈顶指针,
EBP寄存器中保存当前线程的栈底指针。
CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。

 

参考:http://blog.csdn.net/feng_zh/article/details/7075986
————————————————
版权声明:本文为CSDN博主「unix21」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/unix21/article/details/8450155

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值