把C语言编译成汇编是什么样子:6、 如何用汇编程序实现【指针】功能(4)——【函数调用与指针】?

187 阅读4分钟

这时我目前最想深究的问题,我的核心问题有以下几个

1. 当我调用子函数传递的是父函数的局部变量的指针时,汇编如何表现?
2. 当我给子函数传递的是数组局部变量指针时,汇编如何表现?二维数组呢?
3. 当子函数返回值是子函数局部变量的指针时,会怎样?因为此时子函数已经执行完毕,它内部的局部变量已经释放掉,此时如果父函数通过指针修改子函数的局部变量会怎么样?
4. 当我传的参数有结构体的指针时,会怎样?
5. 当我传的参数是全局变量的指针时,会怎么样?

按照上述的问题,直接设计一个包含所有情况的子函数,C代码如下:

#include<stdio.h>

double glb_data = 88.67; // 全局变量

struct Data{
    int a;
    int * p;
};

int* func(int *p, double *p_glb, int arr[], int arr2[2][2], struct Data *p_struct)
{
    int local = *p;
    double j = *p_glb;
    arr[0] = 0;
    arr2[1][1] = 89;
    int k = p_struct->a;
    return &local;
}
int main(){
    int i = 15;
    
    int arr[3] = {1, 2, 3};
    int arr2[2][2] = {
        {1, 2},
        {3, 4}
    };
    struct Data data;
    data.a = 33;
    data.p = &data.a;
    
    int * temp = func(&i, &glb_data, arr, arr2, &data);
    *temp = 12;
    return 0;
}

下面是该C代码对应的汇编程序,我全部粘贴出来一句一句注释解释:

	.file	"point.c"
	.text
	.globl	glb_data
	.data
	.align 8
	.type	glb_data, @object
	.size	glb_data, 8
glb_data:
	.long	1202590843
	.long	1079388897
	.text
	.globl	func
	.type	func, @function
func:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp # 备份父函数的栈帧空间地址
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$80, %rsp # 给子函数func()分配栈帧空间

	# 1 拿出函数的参数 (# `%rdi`,`%rsi`,`%rdx`,`%rcx`,`%r8`和`%r9`)
	movq	%rdi, -40(%rbp)
	movq	%rsi, -48(%rbp)
	movq	%rdx, -56(%rbp)
	movq	%rcx, -64(%rbp)
	movq	%r8, -72(%rbp)

	# 2 备份栈空间保护数据
	movq	%fs:40, %rax
	movq	%rax, -8(%rbp)
	xorl	%eax, %eax

	# 3 int local = *p;
	movq	-40(%rbp), %rax # 拿出地址
	movl	(%rax), %eax # 转成将该地址处的数据取出来,转成32位
	movl	%eax, -24(%rbp) # 给local局部变量分配内存

	# 4 double j = *p_glb;
	movq	-48(%rbp), %rax # 拿出地址
	movsd	(%rax), %xmm0 # 取出该地址对应的内存中的数据,放到xmm0寄存器
	movsd	%xmm0, -16(%rbp) # 给j分配内存到 rbp-16处

	# 5 arr[0] = 0
	movq	-56(%rbp), %rax # 将arr地址拿出来放到rax里
	movl	$0, (%rax) # 将0放到arr地址执行的内存处

	# 6 arr2[1][1] = 89
	movq	-64(%rbp), %rax # 拿出arr2的第0行的首地址
	addq	$8, %rax  # rax = rax + 8,即从二维数组第0行移动到第1行首地址
	movl	$89, 4(%rax) # 把89放到(rax + 4)地址处,此时rax+4是二维数组第1个元素的地址

	# 7 int k = p_struct->a;
	movq	-72(%rbp), %rax # 拿出结构体的首地址
	movl	(%rax), %eax # 结构体第一个元素的值放到rax里
	movl	%eax, -20(%rbp) # 给k分配内存

	# 最后一步,检查栈空间保护数据有没有出错!
        
	# 等等,发现没有,这里没有返回local局部变量的指针!
        
	# 最后,我发现,main里收到的func局部变量的返回值是nil,且程序执行时会报dump错误,
        #    就是因为这里并没有给main函数返回局部变量的地址!
        
	movl	$0, %eax
	movq	-8(%rbp), %rdx
	xorq	%fs:40, %rdx
	je	.L3
	call	__stack_chk_fail@PLT
.L3:
	leave # 释放func函数的栈帧空间,且恢复main函数的栈帧地址,相当于movq %rbp, %rsp     popq %rbp

	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	func, .-func
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6

	# 1. 给main函数分配栈帧空间
	subq	$80, %rsp 

	# 2. 备份栈空间保护数据
	movq	%fs:40, %rax
	movq	%rax, -8(%rbp)
	xorl	%eax, %eax # 自己异或自己,清零

	# 3.int i = 15分配内存
	movl	$15, -68(%rbp)

	# 4. 给一维数组arr分配内存
	movl	$1, -44(%rbp)
	movl	$2, -40(%rbp)
	movl	$3, -36(%rbp)

	# 5. 给二维数组arr2分配内存
	movl	$1, -32(%rbp)
	movl	$2, -28(%rbp)
	movl	$3, -24(%rbp)
	movl	$4, -20(%rbp)

	# 6.struct Data data分配内存
	movl	$33, -64(%rbp) # 给data.a分配内存
	leaq	-64(%rbp), %rax # 算出data.a的内存地址,放到rax寄存器里
	movq	%rax, -56(%rbp) # 给data.p分配内存

	# 7. 调用子函数func之前,先把要传给func参数算出来,放到参数寄存器中
	# 在x86架构中,前6个整型参数会通过寄存器传递。
	# `%rdi`,`%rsi`,`%rdx`,`%rcx`,`%r8`和`%r9`

	leaq	-64(%rbp), %rsi # 结构体的绝对内存地址
	leaq	-32(%rbp), %rcx # 二维数组的绝对内存地址 第4个参数
	leaq	-44(%rbp), %rdx # 一维数组的绝对内存地址 第3个参数
	leaq	-68(%rbp), %rax # i的绝对内存地址
	movq	%rsi, %r8 # 结构体地址放到r8寄存器 第5个参数
	leaq	glb_data(%rip), %rsi # 全局变量的绝对地址放到rsi 第2个参数
	movq	%rax, %rdi # i的绝对地址放到rdi 第1个参数
	call	func # 前5个参数都已经分别放到了`%rdi`,`%rsi`,`%rdx`,`%rcx`,`%r8`寄存器中,此时可以调用func子函数
	movq	%rax, -76(%rbp) # 将子函数的返回值拿出来,放到rbp-76内存里,但是刚才的子函数并没有向rax里面写数据,所以这里取出来的数据是错的,后面执行会出错!
	movq	-72(%rbp), %rax
	movl	$12, (%rax) #将12写进“子函数局部变量内存单元中”,此时会报“Segmentation fault (core dumped)”的错误!
        
        
	# 子程序结束,检查栈空间保护数据是否异常
	movl	$0, %eax
	movq	-8(%rbp), %rdi
	xorq	%fs:40, %rdi
	je	.L6
	call	__stack_chk_fail@PLT
.L6:
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	 1f - 0f
	.long	 4f - 1f
	.long	 5
0:
	.string	 "GNU"
1:
	.align 8
	.long	 0xc0000002
	.long	 3f - 2f
2:
	.long	 0x3
3:
	.align 8
4:

经过上述分析,我再一次体会到了编译器的强大。它把所有的相对地址、绝对地址统统提前算好了,给子函数传指针参数时,传的也都是变量的绝对内存地址,然后直接在子函数里读取就能拿到对应的数据。

值得一提的是,参数是数组时,会传数组的第0行首地址,而具体访问数组的某个元素,编译器会根据你的下标算出来相对位移,比如这里的arr2[1][1],最后算出来的相对于数组第0行首地址的偏移,然后算出对应元素的绝对地址,最后再进行操作。

另外,顺便还测了下全局变量的地址指针问题,最后发现原来全局变量也是一个标号,按标号索引,存放在代码段里。而局部变量是存放在函数的栈帧里,用完就释放,这就是局部变量和全局变量的本质区别。初学高级语言时,只知道局部变量和全局变量的作用域问题,其实基于汇编指令实现的抽象概念。本质是它们放在不同的内存区域,全局变量常驻内存,局部变量用完就释放!

总之,通过上述可以发现,编译器计算了大量的绝对地址和相对地址,这是个辛苦又细致的活,不得不佩服编译器大佬做的工作!