Lesson06 PoEdu培训第一课 计算机科学篇(6) 汇编和可执行文件 随堂作业
文章类别: 培训作业 3 评论

Lesson06 PoEdu培训第一课 计算机科学篇(6) 汇编和可执行文件 随堂作业

文章类别: 培训作业 3 评论

汇编与可执行文件作业

作业题目

    使用VS动态跟踪并记录一个简单的函数调用时栈的变化.

作业

作业程序准备

首先, 我们来看一下作业程序的源代码.

#include <stdio.h>

int main ()
{
    printf("Welcome to Hades Studio, Http://www.hxhlb.net/ !");
    return 0;
}
    我们在第5行, 也就是printf这一句打上断点, 如下图:

Alt 断点

    接下来,为了能够更好的跟踪程序, 减少影响, 我们使用Release版调试.

作业环境准备

    F5运行, 程序自动在断点处停止

需要我们做的工作:

以下所有截图均为英文版, 中文版请按照字面意思寻找.

以下操作均需要在调试模式下操作.

  1. 打开反汇编窗口.

右键当前窗口, 选择"转到反汇编", 如图.

Alt 转到反汇编

  1. 打开内存窗口.

选择调试菜单, 窗口, 内存, 内存窗口*, 如图.

Alt 转到内存

  1. 打开寄存器窗口.

选择调试菜单, 窗口, 寄存器, 如图.

Alt 转到寄存器

作业解答

    首先我们观察反汇编窗口, 看到如下图1所示:

Alt 反汇编1

    可以看到, 我们的代码已经被编译成3句汇编语句.
    图中的 push 指令, 代表我们的字符串入栈. 
        正好印证了老师所讲, 栈中存放有我们需要传递的参数.
    接下来一句就是 call 指令, 我们首先F11, 执行到00FB1045处
    并记录观察寄存器窗口和内存窗口.
    
    我们知道, EBP是栈底, ESP是所用栈的栈顶.
    如图2:

Alt 反汇编2

    可以看到目前的值, 
        ESP = 004FFAF4 (栈顶)
        EBP = 004FFB3C (栈底)
        call 所指向的地址为 0x00FB1010
    接下来, 我们继续F11, 跟着进入这个printf函数处.
    如图3:

Alt 反汇编3

    可以发现, 我们已经跳入到 00FB1010 地址处了.
    我们的ESP(栈顶)也发生了变化, 变成了 004FFAF0, 少了4byte, 
    这也正符合我们栈的结构, 先进后出, "自下而上"的排列.
    我们的EBP(栈底)并没有发生变化.

在这里我们需要想一下, 为什么进行了一个 call 之后, 栈顶会有变化?

我们在 call 的时候, 并没有进行 PUSH 指令, 那我们的栈顶为什么变了呢?

因为我们的栈还保存了跳转后的返回地址, 所以我们的栈顶变化了.

    我们在来查看下现在 ESP(栈顶)处的内存. 如图4:

Alt 反汇编4

    我在图中标出了我们多的这4byte的数据.
    这里需要注意的是, 由于计算机是逆序存储的, 字的高位字节在低地址, 低位字节在高地址.
        如果不懂没关系, 知道就行了, 因为这影响我们看上边的结果.
    我们可以看到, 值为 4a 10 fb 00, 
    由于上边的倒序存放原则, 那么真实的值应该是 00FB104A.
    这是个什么东西呢? 让我们回到  图1 来仔细看看, 
    这个 00FB104A 是不是就是 call 指令的下一句指令的地址?
    这就是编译器编译出来的代码中, 自动为我们完成的 PUSH 跳转后的返回地址 指令操作.

接下来, 我们进入到了 printf 这个函数内了.

    让我们在F11往下执行, 如图5:

Alt 反汇编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

Alt 反汇编6
Alt 反汇编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:

Alt 反汇编8

    那我们在来算算我们现在ESP是多少.
    经过2次POP, ESP = 004FFAE8 + 0x8 = 004FFAF0 
    NICE, 是不是完全正确的?

现在, 我们执行RET指令

执行结果如图9:

Alt 反汇编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取出, 恢复原函数执行上下文.

问题

  1. 是call指令将 "当前call指令的下一句指令的地址" 入栈的吗?

VS调试中并没有发现该汇编码, 但是 "当前call指令的下一句指令的地址" 确实入栈,

不知道是什么函数或指令在什么时候通过什么调用的?

  1. 同样, 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  

如有错误,请提出指正!谢谢.

回复 / Cancel Reply
  1. v_link

    然后你再看一下call指令是怎么工作的。你的问题应该就能解决了。

    回复
  2. v_link

    你看一下eip寄存器的功能。

    回复
    1. @v_link

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

      回复