C++:如何用简单的汇编指令,实现C++复杂抽象的面向对象概念?——「6、动态内存(new/delete)」

122 阅读3分钟

动态内存分配是在堆上分配的,且自己分配的,必须要自己释放,否则就会内存泄漏,具体C++怎么实现的呢?

老规矩,先上C++验证代码:

#include<iostream>
// #include <cstdlib>
/**
 * 内存管理:堆区内存分配
*/


int main(){
    int *arr = new int[11];
    arr[0] = 0;
    arr[9] = 9;
    arr[88] = 88;// 越界
    // std::cout<<arr[88] << std::endl;
    delete[] arr;

    int* arr_malloc = (int*)malloc(11 * sizeof(int));
    arr_malloc[0] = 0;
    arr_malloc[9] = 9;
    arr_malloc[88] = 88;// 越界
    free(arr_malloc);
    return 0;
}

上面C++代码,分别用new/delete分配释放堆内存,又用malloc/free分配释放内存,且还进行了越界赋值,看看什么效果。,下面是对应的汇编:

main:
.LFB1522:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp

# new/delete
	movl	$44, %edi # 申请分配44个字节
	call	_Znam@PLT
	movq	%rax, -16(%rbp) # rax里是函数返回值,rbp-8存储这块堆内存的地址
	
	movq	-16(%rbp), %rax
	movl	$0, (%rax) # 堆内存+0的地址赋值为0,即第0个元素
	
	movq	-16(%rbp), %rax
	addq	$36, %rax
	movl	$9, (%rax) # # 9 * 4 == 36,即第9个元素

	movq	-16(%rbp), %rax
	addq	$352, %rax # 88 * 4 = 352
	movl	$88, (%rax) # 越界赋值88


	cmpq	$0, -16(%rbp) # 空指针检查, 如果相等,那么零标志(ZF)将被设置为1,否则将被设置为0。
	je	.L2 # je .L2指令检查零标志,如果它为1,则跳转到.L2标签处
	movq	-16(%rbp), %rax
	movq	%rax, %rdi
	call	_ZdaPv@PLT


# malloc/free
.L2:
	movl	$44, %edi
	call	malloc@PLT # malloc分配44字节
	movq	%rax, -8(%rbp)
	movq	-8(%rbp), %rax
	movl	$0, (%rax)
	movq	-8(%rbp), %rax
	addq	$36, %rax
	movl	$9, (%rax)
	movq	-8(%rbp), %rax
	addq	$352, %rax # 越界赋值
	movl	$88, (%rax) 
	movq	-8(%rbp), %rax
	movq	%rax, %rdi
	call	free@PLT # 不做空指针检查,直接调用free去释放
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

根据上述汇编可知,汇编上直接调用的是系统调用的动态链接库,具体new/delete、malloc/free的实现代码,需要自己看内核源码,回头研究研究。但是基本上我们可以宏观知道,C++编译器只管调用系统调用,去给你分配释放内存,而且必须是你自己执行delete/free了之后,它才会取调用对应的系统调用去释放对应的堆内存,如果你C++代码里不执行delete/free,那么C++编译器也不会帮你检查。

所以,应用堆内存,一定要小心,注意释放,否则就会出现内存泄漏问题。

另外,越界问题C++也不会帮你检查,直接就从数组的起始位置计算第88个元素的地址,然后直接帮你越界赋值了,其实这是错的,如果真实的程序中,可能会破坏有效数据。所以要注意数组下标的控制。

现在明白了,堆内存的分配与释放也是比较简单的。

总结

这几篇文章,分别探索了栈内存、数据段、只读数据段、代码段、堆内存的分配问题,我现在是比较直观的理解了这些内存的分配情况是怎么回事。

简而言之,栈内存、数据段、代码段的内存分配、内存的相对地址,都是编译器提前计算好的,即都是在编译期确定好的。从汇编程序里也可以看出,所以的标号的寻址用的都是相对地址,相对于rbp寄存器或者rip的地址。

而堆内存的分配,是动态的,当调用系统调用进行分配的时候,系统调用里的代码会自动寻找一块区域分配给用户程序,每一次分配都是不确定的,所以它无法在编译期进行确定!