栈的理解
由于主函数在执行过程中,需要从主函数跳到被调用的函数,这就涉及到保存当前函数状态,进入被调函数的操作。这个保存状态的操作就需要用到栈。
栈的结构如下:
关于栈的理解有几点要注意的:
- 栈是由高地址向低地址方向增长
- rsp是堆栈寄存器,里面存放的是栈顶指针的地址
- 栈顶指针只有在需要扩大栈大小或者缩小栈大小的时候,才会向低地址移动,否则向入栈和出栈是根据栈顶指针的偏移量来操作的,栈顶指针不需要移动。
示例代码
主函数call_proc.c
#include"proc.h"
long call_proc()
{
long x1 = 1; int x2 = 2;
short x3 = 3; char x4 = 4;
proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
return (x1+x2)*(x3-x4);
}
被调函数proc.c
void proc(long a1, long *a1p,
int a2, int *a2p,
short a3, short *a3p,
char a4, char *a4p)
{
*a1p += a1;
*a2p += a2;
*a3p += a3;
*a4p += a4;
}
汇编代码
利用执行gcc -Og -S xxx.c分别生成两者的汇编代码call_proc.s和proc.s如下(只列出关键部分):
call_proc.s
.file "call_proc.c"
.text
.globl call_proc
.type call_proc, @function
call_proc:
.LFB0:
.cfi_startproc
endbr64
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
subq $32, %rsp
.cfi_def_cfa_offset 48
movl $40, %ebx
movq %fs:(%rbx), %rax
movq %rax, 24(%rsp)
xorl %eax, %eax
movq $1, 16(%rsp)
movl $2, 12(%rsp)
movw $3, 10(%rsp)
movb $4, 9(%rsp)
leaq 12(%rsp), %rcx
leaq 16(%rsp), %rsi
leaq 9(%rsp), %rax
pushq %rax
.cfi_def_cfa_offset 56
pushq $4
.cfi_def_cfa_offset 64
leaq 26(%rsp), %r9
movl $3, %r8d
movl $2, %edx
movl $1, %edi
call proc@PLT
movslq 28(%rsp), %rcx
addq 32(%rsp), %rcx
movswl 26(%rsp), %edx
movsbl 25(%rsp), %eax
subl %eax, %edx
movslq %edx, %rax
imulq %rcx, %rax
addq $16, %rsp
.cfi_def_cfa_offset 48
movq 24(%rsp), %rdi
xorq %fs:(%rbx), %rdi
jne .L4
addq $32, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 16
popq %rbx
.cfi_def_cfa_offset 8
ret
proc.s
.file "proc.c"
.text
.globl proc
.type proc, @function
proc:
.LFB0:
.cfi_startproc
endbr64
movq 16(%rsp), %rax
addq %rdi, (%rsi)
addl %edx, (%rcx)
addw %r8w, (%r9)
movl 8(%rsp), %edx
addb %dl, (%rax)
ret
.cfi_endproc
汇编代码分析
先分析call_proc.s中的汇编代码
更改栈的大小
subq $32, %rsp //将rsp减少32个字节,因为栈的增长方向是大地址往小地址,相当于增大栈的容量
保存局部变量
//将局部变量压栈,rsp前面的数字是相当于栈顶指针的偏移量
movq $1, 16(%rsp)
movl $2, 12(%rsp)
movw $3, 10(%rsp)
movb $4, 9(%rsp)
做完上述操作,栈的示意图如下:
保存被调函数的形参
因为该被调函数有8个形参,但是最多只有6个形参能保存到寄存器中,因此有两个被保存到了栈中,且是形参的最后两个值。
//将形参保存到寄存器中
leaq 12(%rsp), %rcx //load effective adress,用于一个内存地址直接赋给目的操作数
leaq 16(%rsp), %rsi
//将形参保存到栈中,
leaq 9(%rsp), %rax
pushq %rax //将寄存器中的数压栈
pushq $4 // 将立即数压栈
//将形参保存到寄存器中
leaq 26(%rsp), %r9
movl $3, %r8d
movl $2, %edx
movl $1, %edi
参数和形参的对应关系如下图所示:
调用被调函数
call proc@PLT
调用被调函数前,会把调用代码的的下一条执行指令压栈,方便从被调函数中返回到主函数时候,还能往下执行。
返回
... //一系列逻辑操作
ret
被调函数汇编代码
逻辑运算
movq 16(%rsp), %rax
// 直接从寄存器中取出运算数
addq %rdi, (%rsi)
addl %edx, (%rcx)
addw %r8w, (%r9)
//从栈中取出运算数
movl 8(%rsp), %edx
addb %dl, (%rax)
返回
ret
总结
- 嵌套的函数调用,需要用栈来保存局部变量、形参、返回时候的指令地址
- C语言的参数是按照从右到左的顺序压到栈中的