本篇文章讲述如何通过vs来分析C++代码。从而能够通过自己写代码,分析代码来熟悉了解C++的对象模型。
在windows环境下,一般我们分析C++代码的时候最常用的工具是vs IDE的工具,在文章如何使用vs查看.obj文件中,我们介绍了怎么查看.obj文件,但是有时候我们使用dumpbin工具查看不如在vs中使用调试查看来的方便,本篇文章使用的环境是vs2022
下面是本篇文章使用的代码
#include <stdio.h>
class A_CLASS
{
public:
char a0;
int a1;
A_CLASS():a0('z'),a1(8) { printf("A construct: %p\n", this); }
};
class B_CLASS
{
public:
int b0 = 1;
B_CLASS(int a)
{
b0 = a;
printf("B construct: %p\n", this);
};
};
class C_CLASS:public B_CLASS
{
public:
int c1=2;
A_CLASS a;
int c0;
C_CLASS():c0(10),B_CLASS(5) { printf("C construct: %p\n", this); };
};
int main(void)
{
C_CLASS c;
}
如果我们在代码执行过程中需要分析代码的执行流程的话,常用的vs工具有三个
可以在调试--窗口--反汇编
点击打开该界面,可以把他拖动到vs底部,这样,每次启动程序的时候都能很方便找到该界面,该界面的样子如下:
需要说明的是:
可以逐语句调试,可以逐过程调试,还支持继续跳转到下一个断点,并且在调试过程中还可以查看每个寄存器的值
,只需要把鼠标放在对应的寄存器名称上即可。如果你的机器默认显示的值是十进制的,可能看起来不太方便,可以在显示的值上右键,选择十六进制显示
,就可以查看十六进制的值啦可以在调试--窗口--寄存器
点击打开该界面,可以把他拖动到vs底部,这样,每次启动程序的时候都能很方便找到该界面,该界面的样子如下:
寄存器界面可以跟随每步的调试动态改变寄存器的值,查看非常方便
注意:寄存器界面默认显示的是通用寄存器的值,我们基本用这些就够了,右键可以添加显示更多寄存器的值
可以在调试--窗口--内存--内存1
点击打开该界面,可以把他拖动到vs底部,这样,每次启动程序的时候都能很方便找到该界面,该界面的样子如下:
内存界面的用处主要在于我们可以输入需要查看的内存地址,就会显示内存中的数据信息
注意:该内存地址是虚拟内存地址,实际的物理内存地址由操作系统维护。
最终,我的调试界面长这样子:
首先我们从main函数打一个断点,启动程序,执行到断点时,查看汇编代码:
00007FF7FB691A20 push rbp
00007FF7FB691A22 push rdi
00007FF7FB691A23 sub rsp,118h
00007FF7FB691A2A lea rbp,[rsp+20h]
00007FF7FB691A2F lea rdi,[rsp+20h]
00007FF7FB691A34 mov ecx,0Eh
00007FF7FB691A39 mov eax,0CCCCCCCCh
00007FF7FB691A3E rep stos dword ptr [rdi]
00007FF7FB691A40 mov rax,qword ptr [__security_cookie (07FF7FB69D000h)]
00007FF7FB691A47 xor rax,rbp
00007FF7FB691A4A mov qword ptr [rbp+0E8h],rax
00007FF7FB691A51 lea rcx,[__A0CBE4BD_main@cpp (07FF7FB6A2008h)]
00007FF7FB691A58 call __CheckForDebuggerJustMyCode (07FF7FB69137Ah)
C_CLASS c;
00007FF7FB691A5D lea rcx,[c]
00007FF7FB691A61 call C_CLASS::C_CLASS (07FF7FB6910CDh)
可以看到,我们虽然在main函数直接执行的C_CLASS c;
,但是编译器添加了很多额外的操作,这些操作我们不需要深究,我们直接看最后两条
00007FF7FB691A5D lea rcx,[c]
00007FF7FB691A61 call C_CLASS::C_CLASS (07FF7FB6910CDh)
因为C_CLASS c;
是局部变量,所以是在栈上分配的空间,在call之前给rcx赋值,一般就是传递参数,并且是第一个参数。我们可以推测这个参数是c的栈上的地址。如果我们换一种测试方法,用下面代码:
int main(void)
{
C_CLASS* c1 = new C_CLASS();
}
对应的汇编代码如下:
C_CLASS* c1 = new C_CLASS();
00007FF77A781B8B mov ecx,10h
00007FF77A781B90 call operator new (07FF77A78103Ch)
00007FF77A781B95 mov qword ptr [rbp+108h],rax
00007FF77A781B9C cmp qword ptr [rbp+108h],0
00007FF77A781BA4 je main+4Bh (07FF77A781BBBh)
00007FF77A781BA6 mov rcx,qword ptr [rbp+108h]
00007FF77A781BAD call C_CLASS::C_CLASS (07FF77A7810F0h)
可以看出,call之前传递给rcx的是[rbp+108h]的值,而[rbp+108h]正是operator new返回的地址,也就是说,起始c的地址在构造方法执行之前已经确定了
,下面看C_CLASS的构造方法的代码
call C_CLASS::C_CLASS
在上面这行打断点,F11跳转到执行的汇编代码处:
00007FF69E3819F0 mov qword ptr [rsp+8],rcx
00007FF69E3819F5 push rbp
00007FF69E3819F6 push rdi
00007FF69E3819F7 sub rsp,0E8h
00007FF69E3819FE lea rbp,[rsp+20h]
00007FF69E381A03 lea rcx,[__A0CBE4BD_main@cpp (07FF69E394008h)]
00007FF69E381A0A call __CheckForDebuggerJustMyCode (07FF69E381406h)
// 调用B_CLASS::B_CLASS,传递两个参数,this地址,和5
00007FF69E381A0F mov edx,5
00007FF69E381A14 mov rcx,qword ptr [this]
00007FF69E381A1B call B_CLASS::B_CLASS (07FF69E3812FDh)
// 给c1赋值
00007FF69E381A20 mov rax,qword ptr [this]
00007FF69E381A27 mov dword ptr [rax+4],2
// 调用A_CLASS::A_CLASS
00007FF69E381A35 add rax,8
00007FF69E381A39 mov rcx,rax
00007FF69E381A3C call A_CLASS::A_CLASS (07FF69E38136Bh)
// 给c0赋值
00007FF69E381A41 mov rax,qword ptr [this]
// 这一行说明A_CLASS占用了8个字节的大小
00007FF69E381A48 mov dword ptr [rax+10h],0Ah
// 调用printf方法,传递两个参数,字符串和this地址
00007FF69E381A4F mov rdx,qword ptr [this]
00007FF69E381A56 lea rcx,[string "C construct: %p\n" (07FF69E38AC58h)]
00007FF69E381A5D call printf (07FF69E3811DBh)
00007FF69E381A62 mov rax,qword ptr [this]
00007FF69E381A69 lea rsp,[rbp+0C8h]
00007FF69E381A70 pop rdi
00007FF69E381A71 pop rbp
00007FF69E381A72 ret
我把上面的汇编代码分为了5个部分:
经过分析,我们很容易得到结论:
最后我们在看一下A_CLASS::A_CLASS的执行代码
00007FF68A891910 mov qword ptr [rsp+8],rcx
00007FF68A891915 push rbp
00007FF68A891916 push rdi
00007FF68A891917 sub rsp,0E8h
00007FF68A89191E lea rbp,[rsp+20h]
00007FF68A891923 lea rcx,[__A0CBE4BD_main@cpp (07FF68A8A4008h)]
00007FF68A89192A call __CheckForDebuggerJustMyCode (07FF68A891406h)
// 从这里开始赋值
00007FF68A89192F mov rax,qword ptr [this]
00007FF68A891936 mov byte ptr [rax],7Ah
00007FF68A891939 mov rax,qword ptr [this]
00007FF68A891940 mov dword ptr [rax+4],8
00007FF68A891947 mov rdx,qword ptr [this]
00007FF68A89194E lea rcx,[string "A construct: %p\n" (07FF68A89AC28h)]
00007FF68A891955 call printf (07FF68A8911DBh)
00007FF68A89195A mov rax,qword ptr [this]
00007FF68A891961 lea rsp,[rbp+0C8h]
00007FF68A891968 pop rdi
00007FF68A891969 pop rbp
00007FF68A89196A ret
从注释的那里开始赋值,可以看到,先给char a0
赋值,地址为rax的地址,然后给int a1
赋值8,可以看到赋值地址为rax+4,这就说明a0占用了四个字节,而不是我们通常理解的1个字节,实际上,如果类中有多个数据成员,某些编译器可能需要内存对齐调整。比如一个char变量和一个int变量,char变量可能会按照4字节内存对齐
上面的例子只是一个简单的关于如何使用vs分析C++代码执行的说明,事实上,我们上面的例子几乎没有用到寄存器和内存两个工具,但是如果我们深入分析一些复杂的逻辑的话,这两个工具还是很有用的。
我们可以向类中添加更多的方法或者变量来分析更复杂的情况,比如:
本篇文章重点在介绍分析的方法,针对于上面这些问题会有专门的文章进行介绍。