汇编语言基础之八 动手练习,将前面的知识用于实践

Debuging栈

这篇文章将使用前面介绍的种种知识,来分析接近真实情况中的栈的分析。你可以照着下面的步骤来操作一次试试。我尽可能用清晰的表达方式,可能有点罗嗦,但是我觉得对于这样细节重重的问题的分析来说,为了让初学者明白,多说几句还是有必要的。我学习的过程中,就遇到了不少简洁描述造成的麻烦,只好前翻后找才能确定过程的细节。我并不假设你能完全记住这个系列前面的文章所讲的内容,但实在看不明白的话,还得麻烦回头去翻翻看。

过程如下

=========

1. 启动WinDBG,打开一个可执行文件。

2. 在一个函数上设置断点。

3. 输入g命令来回到可执行的程序。

4. 操作被debug的程序,使它进入断点。

5. 使用r命令来查看所有的寄存器的值。

6. 查找EIP的值。EIP的值就是设置了的断电的地址。使用ln命令可以查看举例这个地址最近的symbol。即,断点所在的函数名。

7. 使用dd esp命令来查看栈中内存情况。因为ESP是栈顶指针,栈底在内存高地址处。所以dd esp可以展示出栈的内存内容。还记得下面的图示么?esp指向的内存中的内容是一个指针,该指针是母函数调用了子函数,子函数返回后,CPU继续执行母函数的指令地址,也就是图中的Return地址。

8. 在dd esp的命令的返回结果中找到esp指向的内存的内容,使用ln命令来查看这个return地址的symbol,以判断调用当前子函数的母函数是什么。

9. 在步骤5中的运行结果中,找到EBP的内容。结合下图,可以知道,EBP的内容是一个指针,指向栈中的一个位置,而该位置存储的内容是另一个指针,这另一个指针指向母函数的调用者的栈底位置。用数据结构的话来说,这些EBP形成了一个单链表。

2009-11-5 8-00-19

10. 每一个EBP其后紧跟着的一个DWORD,都是当前函数调用结束后,CPU应该继续执行的指令的地址(即返回地址值)。如同步骤8,使用ln命令可以产看再上一级的函数的symbol(函数名)。重复步骤8到步骤10,可以建立出整个callstack。

11. 使用k命令,来检查刚才我们做的操作是否与debugger计算出来的结果一致。

在这里列出一些使用到的WinDBG的命令

命令 英文助记 描述 例子
r Registers Dump出所有的寄存器 r 显示所有寄存器的值
g Go 继续执行程序,类似于VS中的F5 g 从当前断点向后一直执行下去
ln List Nearest symbols 列出最近的symbol。用来查看指针指向的函数,或者当检查一个崩溃的栈的时候,用该命令来查看时哪个函数发出的导致崩溃的函数调用 ln Addr 列出Addr最近的Symbol
d Display or Dereference memory 显示出指针指向的内存的内容,也就是解析指针引用.
dd是指用Double-word值显示内存内容。
dd Addr 以DWORD为单位,显示自Addr开始的内存
k dump stacK Dump出栈中的内容 k 显示出调用栈的全部
u Unassemble 将内存中指定地址的程序代码转换为汇编语言。
u [Range]
Specifies the memory area that contains the instructions to unassemble. If a single address is specified instead of a range, the address will be taken as the beginning of the range, and eight instructions (on an x86 processor) or nine instructions (on an Itanium processor) will be unassembled. If Range is omitted, the disassembly begins at the current address and extends eight or nine instructions.
 
x*! list all modules 列出所有的模块
这里的*是通配符,匹配任意字符串的。
!也是匹配的一部分吧,symbol
x Simple!* 列出Simple模块的所有symbol
x Simple!main列出Simple的mainsymbol

让我们开始一起做一个例子吧。也许这是你第一次使用Windbg,不过没关系,你会看得懂的。

1. 将下面的文件Simple.cpp编译为一个exe文件,然后在Windbg里用File菜单里的Open Executable项来打开。

2. 然后点击File菜单里的Symbol File Path,添加你刚才编译的时候生成的PDB文件的路径。

3. 按ctrl + n打开一个command browser,按alt + 7打开反汇编窗口。

4. 在command browser中敲入命令x*!,得到输出结果如下

start    end        module name
00400000 0041e000   Simple   C (private pdb symbols)  D:\iTaskFolder\Simple\Debug\Simple.pdb
7c800000 7c91e000   kernel32   (export symbols)       C:\WINDOWS\system32\kernel32.dll
7c920000 7c9b6000   ntdll      (export symbols)       C:\WINDOWS\system32\ntdll.dll

5. 现在我们得知了我们的程序的module名字是Simple,我们可以寻找程序入口。输入命令x Simple!*main,得到结果如下。

00401060 Simple!main (void)

6. 现在我们知道了主入口的位置是00401060. 在反汇编窗口上输入这个地址,窗体自动刷新。main函数的第一个汇编指令自动高亮显示了。

7. 在File菜单里选择Open Source File,打开Simple.cpp文件。按F11或F10,你会发现等效的代码运行高亮也显示在源代码窗体中了。就像VS中的debugging一样罗。这里你可以同时看到汇编和源代码的运行对应情况。

8. 我们的目的是为了观察callstack,所以,我们应该在Foo2中设定断点。再看栈的情况。在command browser中,输入命令x Simple!Foo2,得到结果如下。然后拷贝00401137到Disassembly窗体中。代码定位到了Foo2的位置。按F9在这个位置设立断点。同时,源代码窗体代码的相应位置也以红色标识出了断点的存在。

00401137 Simple!Foo2 (int, int)

9. 按F5键,让程序运行到断点上。

10. 输入命令r,查看所有寄存器的情况。结果如下。

eax=00000002 ebx=7ffd5000 ecx=00000002 edx=00000001 esi=00000000 edi=43010000
eip=00401137 esp=0012ff38 ebp=0012ff4c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
Simple!Foo2:
00401137 55              push    ebp

11. 注意,eip的值与Simple!Foo2的symbol地址值相同。说明foo2函数的第一条指令就是cpu准备执行的下一条指令了。测试一下ln命令,输入命令ln 00401137,结果如下

(00401137)   Simple!Foo2   |  (00401270)   Simple!ostream::operator<<

12. 输入命令dd esp,结果如下

0012ff38 00401121 00000001 00000002 00000002
0012ff48  00000001 0012ff64 004010da 0012ff60
0012ff58  0012ff5c 00000002 00000001 0012ff80
0012ff68  00401096 00000001 00000002 00000002
0012ff78  00000001 cccccccc 0012ffc0 00404739
0012ff88  00000001 00420e90 00420de0 43010000
0012ff98  00000000 7ffd5000 00000001 00000006
0012ffa8  0012ff94 80621a58 0012ffe0 00408084

dd

0012ffb8  00417218 00000000 0012fff0 7c817077
0012ffc8  43010000 00000000 7ffd5000 8054c6ed
0012ffd8  0012ffc8 88ff08d8 ffffffff 7c839ad8
0012ffe8  7c817080 00000000 00000000 00000000
0012fff8  00404650 00000000 78746341 00000020
00130008  00000001 000024b8 000000c4 00000000
00130018  00000020 00000000 00000014 00000001
00130028  00000006 00000034 00000114 00000001

13. 按照我们之前的知识来看,00401121应该是调用Foo2的函数Foo1的一个执行地址(Foo2的返回值),输入ln 00401121,发现结果不出所料。注意和这个地址接近的有两个函数,一个是Foo1,一个是Foo2。你可以通过运行x simple!Foo*来确定该地址属于哪个函数。显然该地址小于foo2,大于foo1,由于代码向高地址增长,所以它属于foo1.

C:\Labfiles\Module04\Simple\Simple.cpp(65)+0xd
(004010f0)   Simple!Foo1+0x31   |  (00401137)   Simple!Foo2

14. 注意dd esp命令的结果中的粗体的0012ff64,它的地址是ebp中的值0012ff4c。由于目前刚刚进入函数Foo2,所以Foo2的ebp还没有压栈。当前ebp指向的位置实际上是Foo1的栈底。而Foo1的栈底指向的内存中的值0012ff64应该是函数Foo的栈底。 如果不确定,可以使用d 0012ff4c命令来显示出该内存地址指向的单元的值,0012ff64。再使用dd 0012ff64,得到结果0012ff80。再使用dd 0012ff80, 得到结果0012ffc0。

由此我们查找出了ebp的一个单链表, 紧跟他们后面的一个内存单元的值(返回值)我也列在下面。

0012ff4c –> 0012ff64 –> 0012ff80 –> 0012ffc0

00401121      004010da      00401096       00404739

好,到现在我们已经找到了所有的栈底,和紧跟在他们后面的函数返回值。那么我们练习的结果是否正确呢?

15. 输入命令k,来查看栈。结果如下:

ChildEBP RetAddr 
0012ff34 00401121 Simple!Foo2 [C:\Labfiles\Module04\Simple\Simple.cpp @ 77]
0012ff4c 004010da Simple!Foo1+0x31 [C:\Labfiles\Module04\Simple\Simple.cpp @ 65]
0012ff64 00401096 Simple!Foo+0x2d [C:\Labfiles\Module04\Simple\Simple.cpp @ 49]
0012ff80 00404739 Simple!main+0x36 [C:\Labfiles\Module04\Simple\Simple.cpp @ 33]
0012ffc0 7c817077 Simple!mainCRTStartup+0xe9 [crt0.c @ 206]
WARNING: Stack unwind information not available. Following frames may be wrong.
0012fff0 00000000 kernel32!RegisterWaitForInputIdle+0x49

看来我们的结果完全正确!!!

怎么样?快动手试试看吧?

//  Simple.cpp
#include    <windows.h>
#include    <iostream.h>

BOOL Foo( int, int );
BOOL Foo1( int *, int * );
BOOL Foo2( int, int );

int main()
{
    BOOL sts;

    int i,
    int j;

    i = 1;
    j = 2;
    sts = Foo( i, j );

    return sts;
}

BOOL Foo( int a, int b )
{
    int x = a,
    int y = b;

    Foo1( &x, &y );

    return TRUE;
}

BOOL Foo1( int *a, int *b )
{
    int x = *a,
    int y = *b;

    Foo2( x, y );

    return TRUE;
}

BOOL Foo2( int a, int b )
{
    char response[32];

    cout << "In Foo2." << endl;
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "press enter to continue" << endl;

    cin  >> response;

    return TRUE;
}

原文地址:https://www.cnblogs.com/awpatp/p/1596023.html