Skip to content

Commit

Permalink
Improve c++_object_model
Browse files Browse the repository at this point in the history
  • Loading branch information
selfboot committed Apr 3, 2024
1 parent a291d00 commit 206d8e5
Showing 1 changed file with 14 additions and 12 deletions.
26 changes: 14 additions & 12 deletions source/_drafts/c-object-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ int main() {
这里 int类型在当前平台上占用4个字节(可以用sizeof(int)验证),而这里double成员的起始地址与int成员的起始地址之间相差8个字节,说明在a之后存在**内存对齐填充**(具体取决于编译器的实现细节和平台的对齐要求)。内存对齐要求数据的起始地址在某个特定大小(比如 4、8)的倍数上,这样可以**优化硬件和操作系统访问内存的效率**。这是因为许多处理器**访问对齐的内存地址比访问非对齐地址更快**。
另外在不进行内存对齐的情况下,较大的数据结构可能会跨越多个缓存行或内存页边界,这会导致额外的缓存行或页的加载,降低内存访问效率。不过开发者通常不需要手动管理内存对齐,因为现代编译器和操作系统会自动处理这些问题
另外在不进行内存对齐的情况下,较大的数据结构可能会跨越多个缓存行或内存页边界,这会导致额外的缓存行或页的加载,降低内存访问效率。不过大多时候我们不需要手动管理内存对齐,编译器和操作系统会自动处理这些问题
## 带方法的对象内存分布
接着上面的例子,在类中增加一个方法 setB,用来设置其中成员 b 的值。
带有方法的类又是什么样呢?接着上面的例子,在类中增加一个方法 setB,用来设置其中成员 b 的值。
```c++
#include <iostream>
Expand Down Expand Up @@ -100,13 +100,13 @@ $7 = (void (*)(Basic * const, double)) 0x5555555551d2 <Basic::setB(double)>
![成员方法找到变量地址的汇编代码](https://slefboot-1251736664.file.myqcloud.com/20240330_c++_object_model_method_disassemble.png)
这里的汇编代码**展示了如何通过 this指针和偏移量访问b**。可以分为两部分,第一部分是处理 this 指针和参数,第二部分是找到成 b 的内存位置然后进行赋值。
这里的汇编代码**展示了如何通过 this指针和偏移量访问b**。可以分为两部分,第一部分是处理 this 指针和参数,第二部分是找到成员 b 的内存位置然后进行赋值。
**参数传递部分**。这里`mov %rdi,-0x8(%rbp)`将 this 指针(通过rdi寄存器传入)保存到栈上。将 double 类型的参数value 通过xmm0寄存器传入保存到栈上。这里是 x86_64 机器下 GCC 编译器的传参规定,我们可以通过打印 `$rdi` 保存的地址来验证确实是 temp 对象的开始地址。
**参数传递部分**。这里`mov %rdi,-0x8(%rbp)`将 this 指针(通过rdi寄存器传入)保存到栈上。将 double 类型的参数value 通过xmm0寄存器传入保存到栈上。这是 x86_64 机器下 GCC 编译器的传参规定,我们可以通过打印 `$rdi` 保存的地址来验证确实是 temp 对象的开始地址。
**对象赋值部分**。`mov -0x8(%rbp),%rax` 将this指针从栈上加载到 rax 寄存器中。`movsd -0x10(%rbp),%xmm0` 将参数value从栈上重新加载到xmm0寄存器中。`movsd %xmm0,0x8(%rax)` 将value写入到this对象的 b 成员。这里 `0x8(%rax)` 表示rax(即this指针)**加上8字节的偏移,这个偏移正是成员变量b在Basic对象中的位置**。
**对象赋值部分**。`mov -0x8(%rbp),%rax` 将this指针从栈上加载到 rax 寄存器中。类似的,`movsd -0x10(%rbp),%xmm0` 将参数value从栈上重新加载到xmm0寄存器中。`movsd %xmm0,0x8(%rax)` 将value写入到this对象的 b 成员。这里 `0x8(%rax)` 表示rax(即this指针)**加上8字节的偏移,这个偏移正是成员变量b在Basic对象中的位置**。
这个偏移是什么时候,怎么算出来的呢?其实成员变量的地址相对于对象地址是固定的,对象的地址加上成员变量在对象内的偏移量就是成员变量的实际地址。**编译器在编译时,基于类定义中成员变量的声明顺序和编译器的内存布局规则,计算每个成员变量相对于对象起始地址的偏移量。**这使得在运行时,通过基地址(即对象的地址)加上偏移量,就能够计算出每个成员变量的准确地址。这个过程对于程序员来说是透明的,由编译器和运行时系统自动处理。
这个偏移是什么时候,怎么算出来的呢?其实成员变量的地址相对于对象地址是固定的,对象的地址加上成员变量在对象内的偏移量就是成员变量的实际地址。**编译器在编译时,基于类定义中成员变量的声明顺序和编译器的内存布局规则,计算每个成员变量相对于对象起始地址的偏移量。**然后在运行时,通过基地址(即对象的地址)加上偏移量,就能够计算出每个成员变量的准确地址。这个过程对于程序员来说是透明的,由编译器和运行时系统自动处理。
### 函数调用约定与优化
Expand All @@ -116,7 +116,7 @@ $7 = (void (*)(Basic * const, double)) 0x5555555551d2 <Basic::setB(double)>
接着又将`-0x8(%rbp)` 放到 rax 寄存器,然后再通过`movsd %xmm0,0x8(%rax)`写入成员变量b的值,为啥不直接从`xmm0`寄存器写到基于rbp的偏移地址呢?这是因为 x86_64 的指令集和其操作模式通常支持使用**寄存器间接寻址方式访问数据**。使用`rax`等通用寄存器作为中间步骤,是一种更通用和兼容的方法。
上面的汇编是在**没有开启优化**的情况下,所以编译器采用了直接但效率不是最高的代码生成策略,包括将参数和局部变量频繁地在栈与寄存器间移动。**而编译器的优化策略可能会影响参数的处理方式**。如果我们开启编译优化,如下:
当然上面编译过程**没有开启编译优化**,所以编译器采用了直接但效率不高的代码生成策略,包括将参数和局部变量频繁地在栈与寄存器间移动。**而编译器的优化策略可能会影响参数的处理方式**。如果我们开启编译优化,如下:
```shell
$ g++ basic_method.cpp -o basic_method_O2 -O2 -g -std=c++11
Expand Down Expand Up @@ -174,16 +174,18 @@ int main() {
![带 private 成员的内存布局](https://slefboot-1251736664.file.myqcloud.com/20240401_c++_object_model_method_private.png)
那么 **private 是在运行期还是编译期进行可见性控制的**呢?首先编译期肯定是有保护的,这个很容易验证,我们无法直接访问 temp.c ,或者调用 secret 方法,因为直接会编译出错。
那么 **private 怎么进行可见性控制的呢?**呢?首先编译期肯定是有保护的,这个很容易验证,我们无法直接访问 temp.c ,或者调用 secret 方法,因为直接会编译出错。
那么运行期是否有保护呢?我们来验证下。前面已经验证 private 成员变量也是根据偏移来找到内存位置的,我们可以在代码中直接根据偏移找到内存位置并更改里面的值。
那么**运行期是否有保护呢?**我们来验证下。前面已经验证 private 成员变量也是根据偏移来找到内存位置的,我们可以在代码中直接根据偏移找到内存位置并更改里面的值。
```c++
int* pC = reinterpret_cast<int*>(reinterpret_cast<char*>(&temp) + 16);
*pC = 12; // 直接修改c的值
```

这里修改后,可以增加一个show方法打印所有成员的值,发现这里temp.c 确实被改为了 12。私有方法和普通成员方法一样存储在文本段,我们拿到其地址后,可以通过这个地址调用吗?这里需要一些骚操作,我们**在类定义中添加额外的接口来暴露私有成员方法的地址**,然后通过成员函数指针来调用私有成员函数。整体代码如下:
这里修改后,可以增加一个show方法打印所有成员的值,发现这里temp.c 确实被改为了 12。可见**成员变量在运行期并没有做限制,知道地址就可以绕过编译器的限制进行读写了**。那么私有的方法呢?

私有方法和普通成员方法一样存储在文本段,我们拿到其地址后,可以通过这个地址调用吗?这里需要一些骚操作,我们**在类定义中添加额外的接口来暴露私有成员方法的地址**,然后通过成员函数指针来调用私有成员函数。整体代码如下:

```c++
class Basic {
Expand Down Expand Up @@ -212,9 +214,9 @@ int main() {
### 静态成员
静态成员变量在类的所有实例之间共享,**不管你创建了多少个类的对象,静态成员变量只有一份数据**。静态成员变量的生命周期从它们被定义的时刻开始,直到程序结束。静态成员方法不依赖于类的任何实例来执行,主要用在工厂方法、单例模式的实例获取方法、或其他与类的特定实例无关的工具函数。
每个熟悉 c++ 类静态成员的人都知道,静态成员变量在类的所有实例之间共享,**不管你创建了多少个类的对象,静态成员变量只有一份数据**。静态成员变量的生命周期从它们被定义的时刻开始,直到程序结束。静态成员方法不依赖于类的任何实例来执行,主要用在工厂方法、单例模式的实例获取方法、或其他与类的特定实例无关的工具函数。
下面以一个具体的例子,来看看静态成员变量和静态成员方法的内存布局以及实现特点。
下面以一个具体的例子,来看看静态成员变量和静态成员方法的内存布局以及实现特点。继续接着前面代码例子,这里省略掉其他无关代码了。
```c++
#include <iostream>
Expand Down

0 comments on commit 206d8e5

Please sign in to comment.