好好学习,天天向上,一流范文网欢迎您!
当前位置: >> 报告 >> 汇报材料 内容页

什么是上下文就是?上下文切换本质上就是这些上下文的切换

在 bprc 中,协程的创建和调度需要进行上下文切换。 所谓上下文就是程序在运行过程中特有的状态,主要包括包含局部变量的栈和各种cpu寄存器。 上下文切换本质上就是这些上下文的切换。 局部变量的栈内容在内存中,不会被替换,即切换时内容不会丢失,而且每个程序都复用CPU寄存器,所以每次切换时都要保存每个栈。 栈对应寄存器的值,而上下文切换的本质是如何保存寄存器的值——下一次执行栈时,先恢复CPU寄存器的值,再执行。

以调用函数为例,分为caller和callee。 在切换栈时,我们可以将寄存器存放在调用者中,也可以将调用者栈的寄存器存放在被调用者中。 因此,为了统一OS使用的寄存器,一般的CPU对寄存器都有规定的要求。

1.1. 寄存器 1.1.1。 x86-64 寄存器

在计算机体系结构的教科书中,寄存器通常被称为寄存器文件,它实际上是CPU上的一个存储区域,但他们更喜欢使用标识符而不是地址。

寄存器集成在CPU上,访问速度比内存快几个数量级。 有了更多的寄存器,GCC就可以用更多的寄存器来代替之前的内存栈,从而大大提高性能。

要将寄存器供自己使用,您必须了解它们的用途。 这些用途涉及函数调用。 x86-64有16个64位寄存器,分别是:%rax, %rbx, %rcx, %rdx, %esi, %edi, %rbp, %rsp, %r8, %r9, %r10, %r11, % r12、%r13、%r14、%r15。 在:

%rax 用作函数返回值。 %rsp 栈指针寄存器,指向栈顶 %rbp 栈帧指针,指向栈底 %rdi、%rsi、%rdx、%rcx、%r8、%r9作为函数参数,对应第一个参数和第二个参数依次。 . . %rbx, %r12, %r13, 用作数据存储, 遵循被调用者的规则, 随便用, 调用子函数前备份, 以防被修改 %r10, %r11 用作数据存储,遵循调用者的使用规则寄存器传输级,简单来说,保存使用前的原始值 %rip:相当于PC指针指向当前指令地址,指向下一条要执行的指令

x86-64 寄存器映射

1.1.2. intel x64 寄存器 RCX、RDX、R8 和 R9 从左到右用于整数和指针参数。 寄存器 RAX、RCX、RDX、R8、R9、R10 和 R11 被认为是易变的,必须在函数调用时被销毁。 RBX、RBP、RDI、RSI、R12、R13、R14、R14 和 R15 必须保存在任何使用它们的函数中。

当参数个数超过寄存器指定的个数时,使用堆栈进行传递。

1.2. 堆栈框架

C语言是面向过程的语言。 它最大的特点是把一个程序分解成若干个过程(函数),例如:入口函数是main,然后调用各个子函数。 在相应的机器语言中,GCC将进程转换成栈帧(frame)。 简单的说,每个栈帧对应一个进程。 在一个典型的栈帧结构中,rbp 指向栈帧的开始,rsp 指向栈顶。

堆栈结构

1.2.1. 函数入口和返回

函数的进入和退出由指令call和ret完成。 举个例子

//test.c
#include
#include 

int foo ( int x )
{
    int array[] = {1,3,5};
    return array[x];
}      
 /* -----  end of function foo  ----- */
int main ( int argc, char *argv[] )
{
    int i = 1;
    int j = foo(i);
    fprintf(stdout, "i=%d,j=%dn", i, j);
    return EXIT_SUCCESS;
}

编译通过以下命令获取

gcc -S -o test.s test.c

main:
	pushq	%rbp #push命令,rsp寄存器的值自动增加
	movq	%rsp, %rbp
	subq	$32, %rsp #这句话说明了栈是高地址向地地址分配
	movl	%edi, -20(%rbp)
	movq	%rsi, -32(%rbp)
	movl	$1, -4(%rbp)
	movl	-4(%rbp), %eax
	movl	%eax, %edi #第一个参数传递给edi
	call	foo
	movl	%eax, -8(%rbp) # 返回值接收
	movq	stdout(%rip), %rax
	movl	-8(%rbp), %ecx
	movl	-4(%rbp), %edx
	movl	$.LC0, %esi
	movq	%rax, %rdi
	movl	$0, %eax
	call	fprintf
	movl	$0, %eax
	leave #由于rsp发生了变化,故需要恢复
	ret 
foo:
	pushq	%rbp
	movq	%rsp, %rbp
	movl	%edi, -20(%rbp)
	movl	$1, -16(%rbp) # 数组的分配
	movl	$3, -12(%rbp)
	movl	$5, -8(%rbp)
	movl	-20(%rbp), %eax
	cltq #将eax的值提升为8字节,即后续可以使用rax,只有x86-64可以使用
	movl	-16(%rbp,%rax,4), %eax #值保存在rax中。-16+rbp+rax*4---数组索引
	popq	%rbp #恢复rbp
	ret  #恢复rip

离开命令等同于:

movl %ebp %esp
popl %ebp

调用指令等同于:

pushq %rip #保存下一条指令(第41行的代码地址)的地址,用于函数返回继续执行
jump foo # 跳转到函数foo

ret 指令等同于:

popq %rip #恢复指令指针寄存器

在汇编中,禁止直接改变rip寄存器的值。 只有 call 和 ret 指令允许间接更改 rip 寄存器的值。

1.2.2. 可选帧指针

刚搞清楚帧指针,你是不是很期待它派上用场,所以你可能会失望,因为大多数程序都添加了优化编译选项:-O2,这几乎是一个普遍的选择。 在这个优化级别,即使是更低的优化级别-O1,帧指针也被去掉了寄存器传输级,即帧指针不再保存在%ebp中,而被用在其他地方。 x86-32时代,当前栈帧总是从保存%ebp开始,空间由运行时决定,通过不断的push和pop改变当前栈帧空间; 从x86-64开始,GCC有了新的选择,优化编译选项——O1,可以让GCC不再使用栈帧指针,即不再使用rbp。

这样,所有的空间都在函数的开头预先分配,不需要栈帧指针; 所有局部变量都可以通过 %rsp 的偏移量访问。 第一个示例使用以下命令编译:

gcc –O1 –S –o test.s test.c 

main:
	subq	$8, %rsp
	movl	$1, %edi
	call	foo
	movl	%eax, %ecx
	movl	$1, %edx
	movl	$.LC0, %esi
	movq	stdout(%rip), %rdi
	movl	$0, %eax
	call	fprintf
	movl	$0, %eax
	addq	$8, %rsp
	ret

foo:
	movl	$1, -16(%rsp)
	movl	$3, -12(%rsp)
	movl	$5, -8(%rsp)
	movslq	%edi, %rdi
	movl	-16(%rsp,%rdi,4), %eax
	ret

分析main函数,GCC分析发现栈帧只需要8个字节,所以进入main后的第一条指令分配空间

subq $8,%rsp

然后在返回上一个栈帧之前,回收空间

addq $8,%rsp

为什么在主函数中没有引用分配的空间? 这是因为GCC考虑到栈帧的对齐要求,特意做了安排。 再看一下 foo 函数,这里可以看出 %rsp 是如何引用堆栈空间的。 等等,你不需要先预分配空间吗? 为什么这里没有预分配,直接引用栈顶外的地址呢?这就涉及到x86-64引入的牛逼特性——可以访问栈顶

1.2.3. 访问栈顶之外

我们可以通过readelf命令查看可执行程序的头信息

可执行文件头信息

红色区域指出了遵循ABI规则的x86-64版本。 它定义了一些规范。 ABI 之后的具体实现应该满足这些规范。 其中规定程序可以使用栈顶以外的128字节地址。 我们发现GCC利用了这个特点,干脆不给foo函数分配栈帧空间,而是直接使用栈帧外的空间。 这个特性和内联函数的区别在于,这就是编译优化的力量,而不是像内联函数那样直接把函数复制到调用的地方。

下面我们看一下生成程序集的命令

gcc –O2 –S –o test.s test.c 

ain:
	subq	$8, %rsp
	movq	stdout(%rip), %rdi
	movl	$3, %ecx
	movl	$1, %edx
	movl	$.LC0, %esi
	xorl	%eax, %eax
	call	fprintf
	xorl	%eax, %eax
	addq	$8, %rsp
	ret

直接优化foo函数,结果是3。

1.2.4. 注册保存约定

在过程调用中,调用者的栈帧需要寄存器来暂存数据,被调用者的栈帧也需要寄存器来暂存数据。 如果调用者使用%rbx,被调用者在使用前需要先保存%rbx,然后在返回调用者栈帧之前恢复%rbx。 遵循此使用规则的寄存器是被调用者保存的寄存器,%rbx 对调用者来说是非易失性的。

相反,调用者使用 %r10 来存储局部变量。 为了子函数调用后能够使用%r10,调用者先保存%r10,等子函数返回后再恢复%r10。 遵循此使用规则的寄存器是调用者保存的寄存器。 对于调用者,%r10 是易变的。

#include 
#include 
void sfact_helper ( long int x, long int * resultp)
{
    if (x<=1)
        *resultp = 1;
    else {
        long int nresult;
        sfact_helper(x-1,&nresult);
        *resultp = x * nresult;
    }
}
/* -----  end of function foo  ----- */
long int sfact ( long int x )
{
    long int result;
    sfact_helper(x, &result);
    return result;
}
/* -----  end of function sfact  ----- */
int main ( int argc, char *argv[] )
{
    int sum = sfact(10);
    fprintf(stdout, "sum=%dn", sum);
    return EXIT_SUCCESS;
}
/* ----------  end of function main  ---------- */

main:
	subq	$8, %rsp
	movl	$10, %edi
	call	sfact
	movl	%eax, %edx
	movl	$.LC0, %esi
	movq	stdout(%rip), %rdi
	movl	$0, %eax
	call	fprintf
	movl	$0, %eax
	addq	$8, %rsp
	ret
sfact:
	subq	$24, %rsp
	leaq	8(%rsp), %rsi
	call	sfact_helper
	movq	8(%rsp), %rax
	addq	$24, %rsp
	ret
sfact_helper: # 函数调用开始
	pushq	%rbp
	pushq	%rbx
	subq	$24, %rsp 
	movq	%rdi, %rbx #使用之前保存rbp,rbx
	movq	%rsi, %rbp
	cmpq	$1, %rdi
	jg	.L2
	movq	$1, (%rsi)
	jmp	.L1
.L2:
	leaq	-1(%rdi), %rdi
	leaq	8(%rsp), %rsi
	call	sfact_helper
	imulq	8(%rsp), %rbx
	movq	%rbx, 0(%rbp)
.L1:
	addq	$24, %rsp
	popq	%rbx #使用之后恢复rbp,rbx
	popq	%rbp
	ret # 函数返回

在函数 sfact_helper 中,使用了寄存器 %rbx 和 %rbp。 在覆盖之前,GCC选择先保存他们的值。 GCC 在函数返回之前依次恢复它们。

1.2.5. 参数传递

在X86时代,参数传递是通过压栈来实现的。 与CPU相比,内存访问太慢; 因此,函数调用的效率不高。 x86-64时代,寄存器数量多,GCC最多可以使用6个寄存器来存储参数(rdi、rsi、rdx、rcx、r8、r9),6个以上的参数仍然由将它们推入堆栈。 了解这些对我们写代码很有帮助,至少有两个启示:

尽量使用6个以下的参数列表,不要刁难GCC。传递大对象时,尽量使用指针或引用。 由于寄存器只有 64 位,只能存储整数值,因此寄存器不能存储大对象。

#include    
#include 

int foo ( int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7 )
{  
    int array[] = {100,200,300,400,500,600,700};
    int sum = array[arg1] + array[arg7];
    return sum;
}       
/* -----  end of function foo  ----- */
int main ( int argc, char *argv[] )
{
    int i = 1;
    int j = foo(0, 1, 2, 3, 4, 5, 6);
    fprintf(stdout, "i=%d,j=%dn", i, j);
    return EXIT_SUCCESS;
}               
/* ----------  end of function main  ---------- */

通过命令 gcc -O1 -S -o test.s test.c 编译为:

main:
	subq	$24, %rsp
	movl	$6, (%rsp)
	movl	$5, %r9d
	movl	$4, %r8d
	movl	$3, %ecx
	movl	$2, %edx
	movl	$1, %esi
	movl	$0, %edi
	call	foo
	movl	%eax, %ecx
	movl	$1, %edx
	movl	$.LC0, %esi
	movq	stdout(%rip), %rdi
	movl	$0, %eax
	call	fprintf
	movl	$0, %eax
	addq	$24, %rsp
	ret

foo:
	movl	$100, -32(%rsp)
	movl	$200, -28(%rsp)
	movl	$300, -24(%rsp)
	movl	$400, -20(%rsp)
	movl	$500, -16(%rsp)
	movl	$600, -12(%rsp)
	movl	$700, -8(%rsp)
	movslq	%edi, %rdi
	movslq	8(%rsp), %rdx
	movl	-32(%rsp,%rdi,4), %eax
	addl	-32(%rsp,%rdx,4), %eax
	ret

main函数在调用foo之前准备好foo的参数,前6个放在寄存器中,第7个放在栈中。

1.2.6. 结构传递参数

#include 
#include  

struct demo_s {
    char var8;
    int  var32;
    long var64;
};
struct demo_s foo (struct demo_s d)
{
    d.var8=8;
    d.var32=32;
    d.var64=64;
    return d;
}      
/* -----  end of function foo  ----- */
int main ( int argc, char *argv[] )
{
    struct demo_s d, result;
    result = foo (d);
    fprintf(stdout, "demo: %d, %d, %ldn", result.var8, result.var32, result.var64);
    return EXIT_SUCCESS;
}               
/* ----------  end of function main  ---------- */

通过命令 gcc -S -o test.s test.c 获取程序集

main:
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$48, %rsp
	movl	%edi, -36(%rbp)
	movq	%rsi, -48(%rbp)
	movq	-16(%rbp), %rdx # 字节对齐结构体后var8和var32共8个字节,都放入到rdx中
	movq	-8(%rbp), %rax #8个字节的var64 放入到rax
	movq	%rdx, %rdi
	movq	%rax, %rsi
	call	foo
	movq	%rax, -32(%rbp)
	movq	%rdx, -24(%rbp)
	movq	-24(%rbp), %rsi
	movl	-28(%rbp), %ecx
	movzbl	-32(%rbp), %eax
	movsbl	%al, %edx
	movq	stdout(%rip), %rax
	movq	%rsi, %r8
	movl	$.LC0, %esi
	movq	%rax, %rdi
	movl	$0, %eax
	call	fprintf
	movl	$0, %eax
	leave
	ret
foo:
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rdi, %rax
	movq	%rsi, %rcx
	movq	%rcx, %rdx
	movq	%rax, -16(%rbp) #构造结构体出来
	movq	%rdx, -8(%rbp)
	movb	$8, -16(%rbp) # 结构体var8第一个字段赋值,
	movl	$32, -12(%rbp) # 结构体var32第二个字段赋值
	movq	$64, -8(%rbp) # 结构体var6第三个字段赋值
	movq	-16(%rbp), %rax #结构体第一个,第二个字段赋值给rax
	movq	-8(%rbp), %rdx #结构体第三个字段赋值给rdx
	popq	%rbp
	ret

所以我们可以回到关于结构的问题:

问题1:如何传递结构体? 分为两部分,var8和var32合并成8个字节大小,放在寄存器%rdi中,var64放在寄存器的%rsi中。 也就是说,结构被分解了。 问题2:结构是如何存储的? 查看 foo 函数的第 15-17 行,注意对结构的引用变为偏移量访问。 这与数组非常相似,只是它的元素可以是可变大小的。 问题3:如何返回结构体,原来%rax起到返回值的作用,现在增加了返回值2:%rdx。 同样,GCC 使用两个寄存器来表示结构。

即使在默认情况下,GCC 仍会尽力使用寄存器。 随着结构越来越大,寄存器不够用了,只能用栈了。

1.3. 参考

Windows x64 调用约定中使用的 R10-R15 寄存器是什么?

本文使用知乎 VSCode 创建发布