从汇编角度看C++类的方法访问类成员的原理

2024-03-31 21:08

本文主要是介绍从汇编角度看C++类的方法访问类成员的原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

C++编译后最终也是生成了机器码,不需要解释器或虚拟机来运行。相比C语言,C++的类大大的方便了代码结构的组织,使得构建大程序简便容易了很多。实例化一个类后,类的成员方法就可以访问这个类的成员了,那么从汇编角度看,到底是如何实现的呢?其实这个原理也十分简单,类实例后的对象从内存上和其所有成员变量组成的结构体是一样的,每个类的方法第一个入参就是把这个结构体的地址穿进去,类的方法通过这样的机制实现了访问类的成员。Python的类中self满天飞机制上也有点类似。下面用一个例子来具体看下:

class Test{
public:int a;int b;int c;int d;int e;Test(int a, int b, int c, int d, int e) {this->a = a;this->b = b;this->c = c;this->d = d;this->e = e;}int fun() {int a1 = a;int b1 = b;int c1 = c;int d1 = d;int e1 = e;return 100;}
};int main()
{Test test(1, 2, 3, 4, 5);test.fun();return 0;
}

上面的代码非常简单,主要看一下反汇编后的汇编代码,用下面命令编译 g++ -g -m64 main.c, 编译生成 a.out ELF文件, 然后反汇编 objdump -dS a.out, 即可看到源代码和反汇编的汇编代码一一对应的呈现, 也可以 gdb a.out, 用 disass /m fun_name 看与源码对应的函数反汇编。

首先看下类Test的大小, Test类型大小就是其成员变量组成的结构体的大小。

(gdb) pt Test
type = class Test {public:int a;int b;int c;int d;int e;Test(int, int, int, int, int);int fun(void);
}
(gdb) p sizeof(Test)
$1 = 20

然后看下Test类构造函数的反汇编,函数调用约定可以看:从汇编看Linux C函数的调用约定和参数传递的细节,按照标准调用约定,第一个入参 %rdi 寄存器并不是代码形参,而就是这个对象本身的地址。

    Test(int a, int b, int c, int d, int e) {//400530:   55                      push   %rbp//400531:   48 89 e5                mov    %rsp,%rbp//400534:   48 89 7d f8             mov    %rdi,-0x8(%rbp)  // 把调用约定参数1,test对应实例的地址//400538:   89 75 f4                mov    %esi,-0xc(%rbp)  // 调用约定里参数2//40053b:   89 55 f0                mov    %edx,-0x10(%rbp) // 调用约定里参数3//40053e:   89 4d ec                mov    %ecx,-0x14(%rbp) // 调用约定里参数4//400541:   44 89 45 e8             mov    %r8d,-0x18(%rbp) // 调用约定里参数5//400545:   44 89 4d e4             mov    %r9d,-0x1c(%rbp) // 调用约定里参数6this->a = a;//400549:   48 8b 45 f8             mov    -0x8(%rbp),%rax//40054d:   8b 55 f4                mov    -0xc(%rbp),%edx//400550:   89 10                   mov    %edx,(%rax)this->b = b;//400552:   48 8b 45 f8             mov    -0x8(%rbp),%rax//400556:   8b 55 f0                mov    -0x10(%rbp),%edx//400559:   89 50 04                mov    %edx,0x4(%rax)this->c = c;//40055c:   48 8b 45 f8             mov    -0x8(%rbp),%rax//400560:   8b 55 ec                mov    -0x14(%rbp),%edx//400563:   89 50 08                mov    %edx,0x8(%rax)this->d = d;//400566:   48 8b 45 f8             mov    -0x8(%rbp),%rax//40056a:   8b 55 e8                mov    -0x18(%rbp),%edx//40056d:   89 50 0c                mov    %edx,0xc(%rax)this->e = e;//400570:   48 8b 45 f8             mov    -0x8(%rbp),%rax//400574:   8b 55 e4                mov    -0x1c(%rbp),%edx//400577:   89 50 10                mov    %edx,0x10(%rax)}//40057a:   5d                      pop    %rbp//40057b:   c3                      retq

然后看下Test成员函数的反汇编,隐含着吧这个类实例的首地址作为第一个参数传了进来:

000000000040057c <_ZN4Test3funEv>:int fun() {//40057c:   55                      push   %rbp//40057d:   48 89 e5                mov    %rsp,%rbp//400580:   48 89 7d d8             mov    %rdi,-0x28(%rbp) // 把调用约定的参数1放到栈中int a1 = a;int b1 = b;//400584:   48 8b 45 d8             mov    -0x28(%rbp),%rax//400588:   8b 00                   mov    (%rax),%eax//40058a:   89 45 ec                mov    %eax,-0x14(%rbp)int c1 = c;//40058d:   48 8b 45 d8             mov    -0x28(%rbp),%rax//400591:   8b 40 04                mov    0x4(%rax),%eax//400594:   89 45 f0                mov    %eax,-0x10(%rbp)int d1 = d;//400597:   48 8b 45 d8             mov    -0x28(%rbp),%rax//40059b:   8b 40 08                mov    0x8(%rax),%eax//40059e:   89 45 f4                mov    %eax,-0xc(%rbp)int e1 = e;//4005a1:   48 8b 45 d8             mov    -0x28(%rbp),%rax//4005a5:   8b 40 0c                mov    0xc(%rax),%eax//4005a8:   89 45 f8                mov    %eax,-0x8(%rbp)return 100;//4005ab:   48 8b 45 d8             mov    -0x28(%rbp),%rax//4005af:   8b 40 10                mov    0x10(%rax),%eax//4005b2:   89 45 fc                mov    %eax,-0x4(%rbp)}//4005b5:   b8 64 00 00 00          mov    $0x64,%eax
};//4005ba:   5d                      pop    %rbp//4005bb:   c3                      retq

最后看下 main 函数如何在栈里定义个 Test对象并调用其方法:

int main()
{//4004ed:   55                      push   %rbp//4004ee:   48 89 e5                mov    %rsp,%rbpTest test(1, 2, 3, 4, 5);//4004f1:   48 83 ec 20             sub    $0x20,%rsp//4004f5:   48 8d 45 e0             lea    -0x20(%rbp),%rax//4004f9:   41 b9 05 00 00 00       mov    $0x5,%r9d  // 调用约定里入参6, 从右向左参数压栈//4004ff:   41 b8 04 00 00 00       mov    $0x4,%r8d  // 调用约定里入参5//400505:   b9 03 00 00 00          mov    $0x3,%ecx  // 调用约定里入参4//40050a:   ba 02 00 00 00          mov    $0x2,%edx  // 调用约定里入参3//40050f:   be 01 00 00 00          mov    $0x1,%esi  // 调用约定里入参2//400514:   48 89 c7                mov    %rax,%rdi  // 调用约定里入参1, test的地址//400517:   e8 14 00 00 00          callq  //400530 <_ZN4TestC1Eiiiii>test.fun();//40051c:   48 8d 45 e0             lea    -0x20(%rbp),%rax//400520:   48 89 c7                mov    %rax,%rdi  // test变量的地址作为了第一个参数//400523:   e8 54 00 00 00          callq  //40057c <_ZN4Test3funEv>return 0;//400528:   b8 00 00 00 00          mov    $0x0,%eax
}//40052d:   c9                      leaveq//40052e:   c3                      retq

通过反汇编C++编译后的ELF文件, 可以很容易看出,C++ 的class对象大小就是其成员变量组成对应接头的大小,class方法(成员函数)可以访问成员变量就是因为每个函数隐式的把对象的首地址传给了成员函数。知道了这点,就更加容易知道this指针的本质,也可以思考python的类self为什么满天飞。

这篇关于从汇编角度看C++类的方法访问类成员的原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/865343

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Nginx设置连接超时并进行测试的方法步骤

《Nginx设置连接超时并进行测试的方法步骤》在高并发场景下,如果客户端与服务器的连接长时间未响应,会占用大量的系统资源,影响其他正常请求的处理效率,为了解决这个问题,可以通过设置Nginx的连接... 目录设置连接超时目的操作步骤测试连接超时测试方法:总结:设置连接超时目的设置客户端与服务器之间的连接

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

C#中读取XML文件的四种常用方法

《C#中读取XML文件的四种常用方法》Xml是Internet环境中跨平台的,依赖于内容的技术,是当前处理结构化文档信息的有力工具,下面我们就来看看C#中读取XML文件的方法都有哪些吧... 目录XML简介格式C#读取XML文件方法使用XmlDocument使用XmlTextReader/XmlTextWr