C语言IT狗工作室C语言&嵌入式

开篇3:戏说程序栈-call指令和ret指令

2019-10-11  本文已影响0人  铁甲万能狗

承接上文,抛出的几个问题?

回到上面的问题,其实如果你理解一点基本的汇编代码的语法话,解析程序栈底层的操作问题,一切都迎刃而解,但我们没有打算用汇编代码来作为一个引例。考虑并不是所有人都有汇编代码的基础,所以本篇会用一个具体的例子分解函数调用过程中每个步骤。

好,回到前文的例子,我们这里需要对前面的过程调用做一个完整的流程说明。

程序栈的执行流

从前文的一个粗略的例子,我们已经知道调用者调用被调用者会用到call指令,被调用者在执行结束时以ret指令返回,我们这里将进一步说明如何支援call和return指令。以下是一段反编译后的代码段

备注:例子中的代码片段引用紫Washinton University 计算机科学的网上公开课section 5的示例,本文做了一些修改和扩充


目前指令指针(又叫%eip指针)指示的地址是call指令所在行的地址是0x804854e,&esp指针指向栈顶位置保存着一个724的整数,可能是该过程的一个参数,该示例描述的是目前将要执行call指令(尚未执行)的程序状态

当执行call 0x8048b90这条语句,被调用者函数位于内存地址0x8048b90的位置,接下来会发生什么呢?
由于此时已经读取了call 0x8048b90这条语句,但我们还没完成对call 0x8048b90的调用,此时%eip指针已经向前指向call指令语句的下一条指令,即会如图变化所示:eip指针存储的内存地址更新为0x8048553

111.png

接下来发生的事情,是将call 0x8048b90语句的下一条指令的地址,并将该地址值压入栈,此时栈的变化如下图所示:这里伴随着push指令的发生两个变化

接下来发生的事情,会跳转到8048b90这个地址即被调用者函数的所在行的起始地址。你必须要知道为什么是8048b90这个地址,而不是其他内存地址?生成这个地址的方式如下图,这种称为相对寻址法

2019-10-11 14-14-47屏幕截图.png
你要注意到指令中的常数是063d,当然示例用的小端机器的表示方法,所以在代码片段里显示为 3d06,但我们逻辑上的表示是063d,编译器会将该内存地址常数和eip指针的当前值作加法生成新的内存地址值,在这里即被调用者函数的内存地址。
可能有人会问,为什么不让编译器自行决定任意一个可用的内存地址呢

对不起!我们不是编译器的设计者,作为C/C++程序员,只要理解到编译器使用的寻址原理,并在知道在生成汇编代码时,编译器已经在底层做了这些工作,我们没必要“打烂沙盘,问到底”。

此时,我们真正在意的是,eip指针已经被替换为0x8048b90这个地址,换句话说,call指令告诉编译器可以执行jmp指令跳转到该地址即被调用者函数本体中的第一条指令,

如下图所示(左边的图例):此时的程序状态包含了如下特征

下图被调用者函数的中间指令集不是本文讨论的内容,因此其中间的指令集我用“...”忽略了,当被调用函数到达该本体的结尾之时,即eip指针指向ret指令所在行的地址0x8048591,如下图(右边的图例)所示,此时的程序状态是:

111.png

跟接着,就弹出栈顶的返回地址(即pop操作),返回地址出栈是为了取得该地址,并跳转到该地址指向原来调用者函数本体紧接call 指令所在行下一条指令。此时程序的状态变化如下

如下图变化所示


111.png

返回值的处理

从上面的示例中,我们都没有谈及到返回值是如何从被调用者传递给调用这函数的。是为了简化上面的示例分析,按照惯例,被调用者函数的返回值会放在eax寄存器中,eax的选择是相当随意的,可能是%ecx或%edx等,具体根据不同的C/C++编译器的实现而定。

上一篇下一篇

猜你喜欢

热点阅读