这时我目前最想深究的问题,我的核心问题有以下几个
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行首地址的偏移,然后算出对应元素的绝对地址,最后再进行操作。
另外,顺便还测了下全局变量的地址指针问题,最后发现原来全局变量也是一个标号,按标号索引,存放在代码段里。而局部变量是存放在函数的栈帧里,用完就释放,这就是局部变量和全局变量的本质区别。初学高级语言时,只知道局部变量和全局变量的作用域问题,其实基于汇编指令实现的抽象概念。本质是它们放在不同的内存区域,全局变量常驻内存,局部变量用完就释放!