如何用vs来分析C++代码

前言

本篇文章讲述如何通过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底部,这样,每次启动程序的时候都能很方便找到该界面,该界面的样子如下:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

需要说明的是:

  • 该界面上面有个查看选项,里面能通过勾选操作来实现调试的很多便利,比如可以打开显示行号,这样就很容易将汇编代码和C++代码对应起来
  • 反汇编界面是可以添加断点的,这是个非常好用的操作,可以逐语句调试,可以逐过程调试,还支持继续跳转到下一个断点,并且在调试过程中还可以查看每个寄存器的值,只需要把鼠标放在对应的寄存器名称上即可。如果你的机器默认显示的值是十进制的,可能看起来不太方便,可以在显示的值上右键,选择十六进制显示,就可以查看十六进制的值啦
    在这里插入图片描述

寄存器

可以在调试--窗口--寄存器点击打开该界面,可以把他拖动到vs底部,这样,每次启动程序的时候都能很方便找到该界面,该界面的样子如下:
在这里插入图片描述

寄存器界面可以跟随每步的调试动态改变寄存器的值,查看非常方便

注意:寄存器界面默认显示的是通用寄存器的值,我们基本用这些就够了,右键可以添加显示更多寄存器的值

内存

可以在调试--窗口--内存--内存1点击打开该界面,可以把他拖动到vs底部,这样,每次启动程序的时候都能很方便找到该界面,该界面的样子如下:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

内存界面的用处主要在于我们可以输入需要查看的内存地址,就会显示内存中的数据信息

注意:该内存地址是虚拟内存地址,实际的物理内存地址由操作系统维护。

最终,我的调试界面长这样子:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析

main

首先我们从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的构造方法的代码

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个部分:

  1. 调用B_CLASS::B_CLASS的构造方法,并且传递了两个参数
    • this地址
    • 实参的值5
  2. 给c1赋值
  3. 调用A_CLASS::A_CLASS的构造方法
  4. 给c0赋值
  5. 调用printf方法,传递两个参数,字符串的内容和this地址

经过分析,我们很容易得到结论:

  • 构造方法执行时先执行父类的构造方法
  • 构造方法在执行大括号内容之前先要初始化成员变量
  • 成员变量的初始化顺序跟声明顺序一致,而不是构造方法写的的初始化顺序

最后我们在看一下A_CLASS::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++代码执行的说明,事实上,我们上面的例子几乎没有用到寄存器和内存两个工具,但是如果我们深入分析一些复杂的逻辑的话,这两个工具还是很有用的。

我们可以向类中添加更多的方法或者变量来分析更复杂的情况,比如:

  • 如果我向类中添加一个虚函数,构造方法的执行会有什么不同,类对象的内存分配会有什么不同
  • 如果我继承多个类,调用父类的构造方法时传递的this指针一样不一样
  • 如果我使用了虚基类,构造方法的执行会有什么不同,类对象的内存分配会有什么不同
  • 如果我添加了静态变量,静态方法,会对类对象造成什么影响

本篇文章重点在介绍分析的方法,针对于上面这些问题会有专门的文章进行介绍。

  • 29
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值