gdb调试查看CALL指令的压栈情况
文章目录
CALL指令时压栈
有这样一段代码,功能是输出字符串,代码如下:
section .data
strHello db "Hello World!", 0Ah
STRLEN equ $-strHello
section .text
global _start
_start:
push STRLEN
push strHello
call print
mov ebx, 0
mov eax, 1
int 0x80
print:
mov ecx, [esp+4] ; 为什么是4,为什么不直接pop ecx
mov edx, [esp+8] ; 为什么是8,为什么不pop edx
; 发起sys_write()系统调用,输出信息
mov ebx, 1
mov eax, 4
int 0x80
ret
上述代码,把输出字符串的代码放到一个子程序print
中,然后在调用print
子程序时,字符串的地址和字符串的长度通过栈来传递。
粗略地一看很简单也没毛病,在call print
前,push STRLEN
和push strHello
是把字符串长度和字符串的地址使用压栈的方式来传递参数,然后在print
子程序中从栈中取出参数,就完成了参数的传递。栈内存就如下分部:
但看到print
子程序代码时,不禁一想,参数压栈了,取参数STRLEN
和strHello
时为什么不直接pop ecx
和pop edx
呢?
然后又看了一下取参数的下标,代码中是mov edx, [esp+8]
和mov ecx, [esp+4]
,strHello
不是应该在栈顶吗,不应该是直接取吗?
当查看了课本之后,才发现,在使用call指令调用子程序时,相当于修改了程序走向,要修改ip的值,因此需要把原来ip的值压栈保存,当子程序调用完毕后在把ip的值恢复。
所以,在call print
前,ip会指向mov ebx, 0
(call指令的下一个指令),那么当执行call指令,就会把这个地址压栈,也就是栈空间分布其实是这样的:
可是书上,写的是“相当于”,是真的就是把ip的值压栈吗?是指SS:SP
那个栈吗?
所以我想在程序运行中验证下栈的数据,确认下当call指令执行完,栈顶是不是那个原先的mov ebx, 0
(call指令的下一个指令)的地址。
使用gdb调试验证CALL指令压栈情况
这里再贴一边代码:
section .data
strHello db "Hello World!", 0Ah
STRLEN equ $-strHello
section .text
global _start
_start:
push STRLEN
push strHello
call print
mov ebx, 0
mov eax, 1
int 0x80
print:
mov ecx, [esp+4] ; 为什么是4,为什么不直接pop ecx
mov edx, [esp+8] ; 为什么是8,为什么不pop edx
; 发起sys_write()系统调用,输出信息
mov ebx, 1
mov eax, 4
int 0x80
ret
将上述的汇编代码生成可执行程序(可调试的,使用参数-g指定):
输入gdb hello
进入gdb调试,对hello进行调试。
进入gdb调试页面,输入b _start
,b print
,在标号为_start
和print
的地方打上断点,如下图:
然后查看输入r
,表示run,然后程序会执行到断点_start
处停止,如下图:
查看_start标号断点执行前的情况
查看反汇编代码
此时,就可以查看_start
标号的子程序代码块的内容了,输入disassemble
,将此处内存的机器码反汇编,查看每一条汇编指令所在的内存地址。
上图的反汇编代码对应的源代码就是:
可以看到:
STRLEN
,strHello
,print
等标号都被替换成了具体的数值。- 每一行指令的地址
可知:
- STRLEN = 0xd = 13,数一数代码中字符个数也正好是13;这个数在调用print时会被压栈。
- strHello表示字符串的地址,该地址是0x804a000,这个地址数值在调用print时会被压栈。
- call指令的下一条指令(
mov ebx, 0
)的地址是0x0804900c
查看print标号断点执行前的情况
执行到print
标号处,是call print
指令已经执行完毕了,已经进入print
子程序了,ip已经指向了print
子程序第一条指令,该指令欲执行。
也就是此时,进入print
子程序了,_start
程序中的下面的三条指令都执行完毕:
push STRLEN
push strHello
call print
查看栈段的内容
此时查看栈段的值,要想知道栈的内容,要先知道esp的值:
上图可知,栈顶指针的地址是0xffffce94,由于栈是往下走的,因此栈顶在低地址,栈底在高地址。
所以,查看地址0xffffce94处连续2个word(1 word = 4 byte)和1个h(1 h = 2 byte),因为在压栈时,STRLEN应该是2个字节,strHello是一个地址占4个字节,压入旧的eip的值也是4个字节。所以预想的栈内存分布图应该如下:
所以接下来只需验证下栈的内容和图中预想是不是一样的即可。
gdb中查看栈的内容(这里为了方便,直接看栈顶连续三个word的内容算了):
x/3xw 0xffffce94
注:0xfffce94是栈顶指针ESP的值,上面图中有。
栈中内容显示结果如下:
可知,此时实际内存中,栈的内容应该是这样的:
在看看上面提到的_start
标号处汇编指令执行过程中的地址情况:
栈中的内容,和我们对CALL指令压栈的猜想完全一致。
结论
所以最终结论是,在调用call 子程序
指令时,会把call执行的下一行指令的地址压栈保存,并且该栈就是进行数据操作的栈,所以在操作栈时要小心,不光自己使用push
、pop
指令会修改栈的内容,在使用call
指令也会修改栈的内容。
因此在CALL指令调用子程序,使用栈传递参数时,入参可以push 参数
,取参数时可不能直接pop
,因为栈顶并不是刚才传进去的参数,而是原始ip的值!