文章目录
函数调用的原理
linux 程序内存布局
传统linux程序(32bit)拥有4G的虚拟内存区域,高1G的区域供内核使用,剩余的3G内存供程序使用。按段划分,主要分程序段(text segement)、数据段、BSS段。BSS段用于未初始化的静态变量的初始化(0值初始化)。栈从高到低地址增长。堆从低到高增长。栈和堆的这两种不同的地址增长方向,需要关注下,后面协程切换中就涉及到该布局的
不同。
栈帧定义与函数调用
两个寄存器
- esp 栈顶指针寄存器,指向调用栈的栈顶(始终指向,意味着栈分配到哪里了,从当前栈往高地址是已被分配了的)
- ebp 基址指针寄存器,指向当前活动栈帧的基址
其他:
X86-64有16个64位寄存器,分别是:
%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。
其中:
- %rax 作为函数返回值使用。
- %rsp 栈指针寄存器,指向栈顶
- %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
- %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
- %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
栈帧
栈帧是从栈上分配的一段内存,每次函数调用时,用于存储自动变量。从物理介质角度看,栈帧是位于esp(栈指针)及ebp(基指针)之间的一块区域。局部变量等分配均在栈帧上分配,函数结束自动释放。
这里我们看一下一个栈帧在内存中的布局:
- 注意在这里相当于是一个倒栈
红色区域是进入新的函数中时,栈帧的内存布局。这里需要关注红色区域之上的参数2,参数1,和返回地址。即每次进行函数调用时,会将参数从右到左的入栈,最后将返回地址入栈。这个返回地址是指,函数调用结束后的下一行执行的代码地址
。获取参数及返回地址通过EBP相对偏移即可获得,图示为32位系统对应的偏移值。
x86_64架构下gnu编译器c/cpp函数调用解析:
首先需要确定的是:
每一次函数调用都是一个栈帧
函数调用流程:
- 1 传参,主要是传递给寄存器。当寄存器不够用时,会丛右到左压栈,然后再传参给寄存器( %rdi,%rsi,%rdx,%rcx,%r8,%r9,这6个不够用的时候才会借用栈。所以rdi是第一个参数,是保存寄存器组内存的首地址)
- 2 将返回地址压栈,该地址一般指向函数返回后的下一条指令
- 3 修改rip寄存器(指令寄存器)为调用函数的起始地址,新的函数开始了
- 4 将上个函数的栈帧基址(rbp寄存器用于存放栈帧基址)压入栈中
- 5 将rbp寄存器中的值修改为rsp寄存中的值,即开启了新的栈帧
- 6 在栈里为当前函数局部变量分配所需的空间,表现为修改ESP寄存器的值
- 7 函数返回时,通过 rbp 的偏移就能够获得原先的参数和返回地址
OK,知道了函数调用的原理,接下来我们来实际的看一看函数的调用过程:使用 GDB
测试程序:
#include<stdio.h>
int func(int a,int b){
int c= 0 ;
int d= 1;
c= a*b;
printf("c is %d \n",c);
}
int main(void)
{
func(1,2); //gdb 在这里停下来
return 0;
}
具体见:https://blog.csdn.net/weijitao/article/details/46794573
disass //展示汇编代码
i reg // 查看寄存器
coctx_swap.S 汇编解读(只关注__x86_64__)
在这个汇编文件定义了函数 coctx_swap 函数(co_routine->coctx_swap())。该函数的作用是 保存当前 co_routine 的执行环境到结构体 coctx_t ,然后将CPU上下文设置为目标co_routine的上下文.
我们先简要看一下协程上下文的定义
//coctx.h
struct coctx_t
{
#if defined(__i386__) //忽略
void *regs[ 8 ];
#else
void *regs[ 14 ];//用于保存或者设定特定寄存器值
#endif
size_t ss_size;//栈帧区域的size
char *ss_sp; //协程栈帧内存区域,这个区域一般在堆上分配
};
14个寄存器,每个寄存器是64位(8字节),所以寄存器组最后一个位置偏移112=13*8字节(下面要用到)
先简单解释一下头部的代码:
.globl coctx_swap //.global 声明coctx_swap是全局可见的
#if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
.type coctx_swap, @function //gnu汇编器定义函数时的规则
#endif
coctx_swap: //coctx_swap函数内容开始
leaq 8(%rsp),%rax
leaq 112(%rdi),%rsp
...
该函数实际被调用时,传入了两个参数,均为coctx_t
类型指针。接下来我们看该函数的上半段
lea是取址指令,b,w,l,q是操作属性限定符,分别表示1字节,2字节,4字节,8字节。
#elif defined(__x86_64__)
//进入函数之前的栈的情况
//(old) rip
//rbp = rsp
//刚开始进入,rbp = rsp
leaq 8(%rsp),%rax // rax=rsp+8,rax 保存了原来的 rip
leaq 112(%rdi),%rsp //rsp 指向原先的 rbp
pushq %rax //保存各个寄存器
pushq %rbx
pushq %rcx
pushq %rdx
pushq -8(%rax) //ret func addr返回原来的 rip
pushq %rsi
pushq %rdi
pushq %rbp
pushq %r8
pushq %r9
pushq %r12
pushq %r13
pushq %r14
pushq %r15
.....
上半段相当于是把当前的各寄存器值存入了第一个参数传入的协程上下文的regs数组中,最后的返回地址放在了regs[9]中,rsi会保存着第二个参数 coctx_t
ok,来看下半部分 (恢复下一个工作协程的上下文):
movq %rsi, %rsp//rsp(存储栈顶的地址,改变它的地址,就相当于改变了栈空间)替换为rsi寄存器中的值
popq %r15//regs数组中的各值恢复到各寄存器中。将返回地址压入栈中
popq %r14
popq %r13
popq %r12
popq %r9
popq %r8
popq %rbp
popq %rdi
popq %rsi
popq %rax //ret func addr regs[9] = rax
popq %rdx
popq %rcx
popq %rbx
popq %rsp
pushq %rax //入栈 rax
xorl %eax, %eax
ret //ret指令约定从当前栈顶弹出返回地址放入IP寄存器,让出控制流。
OK,至此,切换就完成了!!!
windows 8086 汇编实现切换
这是一位大佬写的,我也看不太懂,正在学习 ing …
yield proc
pushf ;先保存一下flags
cmp ax,0 ;判断是否是esc
jne yEls1
mov AH, 4CH ;esc exit
int 21h
yEls1:
;是tab,先在它自己的栈里保存寄存器,
push ax
push bx
push cx
push dx
push di
push si
push bp
;再切换栈,恢复另一组寄存器
cmp yFunc,0 ;当前是tri?
je yTri
mov stack_snake_sp,sp
mov sp,stack_tri_sp ;当前是1:snake,要切换成tri
mov ax,STACK_RTI
mov ss,ax
mov yFunc,0
jmp yTriEd
yTri:
mov stack_tri_sp,sp
mov sp,stack_snake_sp ;当前是0:tri,要切换成snake
mov ax,STACK_SNAKE
mov ss,ax
mov yFunc,1
;恢复寄存器
yTriEd:
pop bp
pop si
pop di
pop dx
pop cx
pop bx
pop ax
popf
ret ;切换
yield endp