汇编与可执行文件作业
作业题目
使用VS动态跟踪并记录一个简单的函数调用时栈的变化.作业
作业程序准备
首先, 我们来看一下作业程序的源代码.
#include <stdio.h>
int main ()
{
printf("Welcome to Hades Studio, Http://www.hxhlb.net/ !");
return 0;
} 我们在第5行, 也就是printf这一句打上断点, 如下图:
接下来,为了能够更好的跟踪程序, 减少影响, 我们使用Release版调试.作业环境准备
F5运行, 程序自动在断点处停止需要我们做的工作:
以下所有截图均为英文版, 中文版请按照字面意思寻找.
以下操作
均需要在调试模式下操作.
- 打开
反汇编窗口.右键当前窗口, 选择"转到反汇编", 如图.
- 打开
内存窗口.选择
调试菜单,窗口,内存,内存窗口*, 如图.
- 打开
寄存器窗口.选择
调试菜单,窗口,寄存器, 如图.
作业解答
首先我们观察反汇编窗口, 看到如下图1所示:
可以看到, 我们的代码已经被编译成3句汇编语句.
图中的 push 指令, 代表我们的字符串入栈.
正好印证了老师所讲, 栈中存放有我们需要传递的参数.
接下来一句就是 call 指令, 我们首先F11, 执行到00FB1045处
并记录观察寄存器窗口和内存窗口.
我们知道, EBP是栈底, ESP是所用栈的栈顶.
如图2:
可以看到目前的值,
ESP = 004FFAF4 (栈顶)
EBP = 004FFB3C (栈底)
call 所指向的地址为 0x00FB1010
接下来, 我们继续F11, 跟着进入这个printf函数处.
如图3:
可以发现, 我们已经跳入到 00FB1010 地址处了.
我们的ESP(栈顶)也发生了变化, 变成了 004FFAF0, 少了4byte,
这也正符合我们栈的结构, 先进后出, "自下而上"的排列.
我们的EBP(栈底)并没有发生变化.在这里我们需要想一下, 为什么进行了一个 call 之后, 栈顶会有变化?
我们在 call 的时候, 并没有进行 PUSH 指令, 那我们的栈顶为什么变了呢?
因为我们的栈还保存了
跳转后的返回地址, 所以我们的栈顶变化了.
我们在来查看下现在 ESP(栈顶)处的内存. 如图4:
我在图中标出了我们多的这4byte的数据.
这里需要注意的是, 由于计算机是逆序存储的, 字的高位字节在低地址, 低位字节在高地址.
如果不懂没关系, 知道就行了, 因为这影响我们看上边的结果.
我们可以看到, 值为 4a 10 fb 00,
由于上边的倒序存放原则, 那么真实的值应该是 00FB104A.
这是个什么东西呢? 让我们回到 图1 来仔细看看,
这个 00FB104A 是不是就是 call 指令的下一句指令的地址?
这就是编译器编译出来的代码中, 自动为我们完成的 PUSH 跳转后的返回地址 指令操作.接下来, 我们进入到了 printf 这个函数内了.
让我们在F11往下执行, 如图5:
让我们来看看图9中这两句话是什么意思.
push我们都知道, 是入栈, 将EBP的值入栈.
我们来想一下, 入栈时, ESP(栈顶)会"向上"移动指针, 那么这时候, ESP = 004FFAF0 - 4 = 004FFAEC
和图9中的ESP地址是一样的吧.
接下来是 mov ebp, esp
这个大家都能理解, 是把ESP(栈顶)的值搬运到EBP(栈底), 就是赋值.
接下来我们想一下, 这EBP是什么玩意儿来着? EBP是栈底, 现在栈底被入栈了!!!
看到这里, 大家要淡定, 这属于系统对我们程序数据的保存机制.
因为我们都知道, 一般, ESP-EBP中间的这一段栈, 我们成为"函数栈".
也就是说, 我们一个函数内部所有的临时变量, 参数等, 都在这一段范围内.
而我们现在, 由main函数跳转至printf函数,
所以系统为我们保存当前main函数的上下文, 将EBP(栈底)入栈保存,
然后将栈底移动到当前栈顶的位置.
经过这样操作之后, 我们接下来的printf函数, 就不会对我们main函数中的任何栈有影响.我们进入printf后, 不在详细跟踪, 我们直接到ret指令处.
如图6, 图7


稍微对代码一说.
我们经过了这么多步之后, 现在看看我们的 ESP(栈顶) 和 EBP(栈底)
ESP = 004FFAE8
EBP = 004FFAEC
可以看到, 我们的EBP并没有变化.
(其实是有变化的, 但是我们没有深入跟踪printf里边的call指令, 所以现在EBP被还原, 我们暂且不管.)
我们的ESP变了不少, 我们来计算一下, 刚开始ESP=004FFAEC,
然后我们的函数经过了8次 PUSH 指令, 两次 add esp 指令,
那么 ESP = 004FFAEC - 0x20(4 * 8 = 32, 16进制20) + 0x18 + 0x4 =
004FFAEC - 0x20 + 0x1C =
004FFAEC - 4 = 004FFAE8
NICE, 结果完全正确!接下来, 我们看看printf函数执行完成后, 是如何返回到main函数的.
首先, 通过图6和图7可以看到, 当前的指令为 POP
POP就是出栈操作, 相应的 ESP9(栈顶) 需要"向下"移动.
我们不关注ESI寄存器, 我们直接往下运行, 发现POP了EBP.
还记得被PUSH的EBP的值吗? 是不是 004FFB3C 这个地址?
不知道了吗? 虽然我忘记在EBP被MOV之前截图, 但是我们在运行main函数的时候, EBP是没变过的.
我们来验证一下, 如图8:
那我们在来算算我们现在ESP是多少.
经过2次POP, ESP = 004FFAE8 + 0x8 = 004FFAF0
NICE, 是不是完全正确的?现在, 我们执行RET指令
执行结果如图9:

我们可以看到, 我们的ESP(栈顶)变成了004FFAF4, 多了4byte.
回想一下上边我们说的, 在call的时候自动少了4byte是因为什么?
是因为编译器生成的汇编指令保存了我们当前call指令的下一句指令的地址.
那反过来就很好理解了.
在执行RET的时候, 我们的编译器又帮我们做事情了.
它帮我们自动生成了汇编指令, POP了我们的地址.
所以, 我们的ESP(栈顶)多了4byte, 而我们的程序也顺利的通过POP的地址, 找到了main函数的正确的执行地址.总结
1. 程序的临时变量(局部变量)一般都是在栈中保存.
2. 调用方法时, 传递的参数也在栈中保存.
3. 在调用方法前, "当前call指令的下一句指令的地址"一定会先PUSH到栈中.
4. 进入方法后, 首先对函数执行上下文进行保存, 执行的是 EBP 的入栈, 然后将ESP的地址赋值给EBP
5. RET时, 会执行POP将PUSH的EBP取出, 恢复原函数执行上下文.问题
- 是call指令将 "当前call指令的下一句指令的地址" 入栈的吗?
VS调试中并没有发现该汇编码, 但是 "当前call指令的下一句指令的地址" 确实入栈,
不知道是什么函数或指令在什么时候通过什么调用的?
- 同样, ret指令将 "当前call指令的下一句指令的地址" 出栈, 与问题1同样的疑惑.
程序的汇编码:
00FB1010 push ebp
00FB1011 mov ebp,esp
00FB1013 push esi
951: int _Result;
952: va_list _ArgList;
953: __crt_va_start(_ArgList, _Format);
954: _Result = _vfprintf_l(stdout, _Format, NULL, _ArgList);
00FB1014 mov esi,dword ptr [_Format]
00FB1017 push 1
00FB1019 call dword ptr [__imp____acrt_iob_func (0FB20B0h)]
00FB101F add esp,4
00FB1022 lea ecx,[ebp+0Ch]
00FB1025 push ecx
00FB1026 push 0
00FB1028 push esi
00FB1029 push eax
00FB102A call __local_stdio_printf_options (0FB1000h)
00FB102F push dword ptr [eax+4]
00FB1032 push dword ptr [eax]
00FB1034 call dword ptr [__imp____stdio_common_vfprintf (0FB20ACh)]
00FB103A add esp,18h
00FB103D pop esi
955: __crt_va_end(_ArgList);
956: return _Result;
957: }
00FB103E pop ebp
00FB103F ret
--- g:\_dev\vs_proj\vs2015_proj\_c++vip\asmdemo\asmdemo\asmdemo.cpp ------------
5: printf("Welcome to Hades Studio, Http://www.hxhlb.net/ !");
00FB1040 push offset string "Welcome to Hades Studio, Http://"... (0FB20F8h)
00FB1045 call printf (0FB1010h)
00FB104A add esp,4
6: return 0;
00FB104D xor eax,eax
7: }
00FB104F ret 如有错误,请提出指正!谢谢.
本文由 花心胡萝卜 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: 2016-10-06 at 03:01 pm



然后你再看一下call指令是怎么工作的。你的问题应该就能解决了。
你看一下eip寄存器的功能。
EIP寄存器是我们的程序计数器, 其实我感觉叫


指令寄存器会更贴切.不知道是不是Mark上课的口误....
它的变化, 是随着我们执行过程不断的变化的.
它的值
总是等我我们下一条即将要执行的指令的地址.我又重新调试了一遍, 截了一些图: