coctx_swap.S、coctx.h、coctx.cpp上下文切换(汇编)—— libco源码分析、学习笔记

博客一级目录

二级目录——libco源码分析/学习笔记

参考大牛博客:传送门

coctx_swap.S大概功能

分析文件:coctx_swap.Scoctx.hcoctx.cpp

协程的调度和线程是很类似的。也需要保存和恢复上下文,这就要牵扯到各种寄存器了,而牵扯到寄存器,就不得不使用汇编指令。coctx_swap.S文件内实现的功能是协程之间上下文保存和切换。

贴一遍代码:

.globl coctx_swap
#if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
.type  coctx_swap, @function
#endif
coctx_swap:

#if defined(__i386__)
	//此处代码不做分析,略去若干代码
#elif defined(__x86_64__)
	leaq 8(%rsp),%rax
	leaq 112(%rdi),%rsp
	pushq %rax
	pushq %rbx
	pushq %rcx
	pushq %rdx

	pushq -8(%rax) //ret func addr

	pushq %rsi
	pushq %rdi
	pushq %rbp
	pushq %r8
	pushq %r9
	pushq %r12
	pushq %r13
	pushq %r14
	pushq %r15
	
	movq %rsi, %rsp
	popq %r15
	popq %r14
	popq %r13
	popq %r12
	popq %r9
	popq %r8
	popq %rbp
	popq %rdi
	popq %rsi
	popq %rax //ret func addr
	popq %rdx
	popq %rcx
	popq %rbx
	popq %rsp
	pushq %rax
	
	xorl %eax, %eax
	ret
#endif

简要看一下协程上下文(协程控制字)的定义

//coctx.h
struct coctx_t
{
#if defined(__i386__)
    void *regs[ 8 ];
#else
    void *regs[ 14 ];
#endif
    size_t ss_size;//协程剩余大小。
    char *ss_sp; //协程栈底,每个协程都有独立的栈空间 ,sp+size=栈顶指针
};

以上结构体(64位部分)主要包含了14个寄存器,每个寄存器是64位(8字节),所以寄存器组最后一个位置偏移112=13*8字节(下面要用到)

值得一提的是,regs的类型是void*类型,并不是说里面存储的是指针类型,而是为了开辟符合机器位长的空间(指针类型大小跟机器位长相同)

我们主要分析x86_64部分,对于x86_64来说,上下文有14个信息:

coctx.cpp
 // 64 bit

 

// | regs[0]: r15 |           low
 // | regs[1]: r14 |
 // | regs[2]: r13 |
 // | regs[3]: r12 |
 // | regs[4]: r9 |
 // | regs[5]: r8 |
 // | regs[6]: rbp |
 // | regs[7]: rdi |
 // | regs[8]: rsi |
 // | regs[9]: ret |  //ret func addr  保存断点地址
 // | regs[10]: rdx |
 // | regs[11]: rcx |
 // | regs[12]: rbx |
 

// | regs[13]: rsp |              hig

.globl coctx_swap
#if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
.type  coctx_swap, @function
#endif
coctx_swap:

这部分是汇编函数声明,global的意思是使函数在其他文件可见。第三行是定义函数的格式。最后一行是函数入口标号

直接转到x86_64部分:

#elif defined(__x86_64__)
	leaq 8(%rsp),%rax 
	leaq 112(%rdi),%rsp

注意,x86_64栈是 满递减结构。

lleaq 8(%rsp),%rax 翻译一下是: rax=rsp+8(取地址,不引用内存),sp是栈顶指针,函数调用进来之前已经保存了上一个函数的各种寄存器,至此原来的函数结束(栈帧结束),然后保存一个断点地址以便返回。于最后的断点地址。rsp当前指向断点地址,+8是栈后退一格就指向了原来的栈顶,所以rax保存了原来的栈顶。

说明一下rax默认是保存返回值,这里显然覆盖掉rax了,至于为什么可以不保存rax的现场?好吧,这个还没弄明白等以后补坑吧。

leaq 112(%rdi),%rsp翻译一下是rsp=rdi+112

x86_64中函数传参优先使用rdi,rsi,rdx,rcx,r8,r9,当这6个不够用的时候才会借用栈。所以rdi是第一个参数,是保存寄存器组内存的首地址。最后一个位置偏移了112=13*8个字节。这句话意思是让rsp指向栈底。接下来要陆续将寄存器入栈了。

        pushq %rax
	pushq %rbx
	pushq %rcx
	pushq %rdx

	pushq -8(%rax) //ret func addr

	pushq %rsi
	pushq %rdi
	pushq %rbp
	pushq %r8
	pushq %r9
	pushq %r12
	pushq %r13
	pushq %r14
	pushq %r15

接下来是一系列入栈操作,注意刚刚rax被赋值为rsp+8,所以第一条语句的意思是保存上一个状态的栈顶指针。

    pushq -8(%rax) //ret func addr这条语句:由于rax保存的rsp+8,函数调用后栈最后一个位置rsp是断点地址,所以rax-8=rsp指向的就是断点地址。

	movq %rsi, %rsp
	popq %r15
	popq %r14
	popq %r13
	popq %r12
	popq %r9
	popq %r8
	popq %rbp
	popq %rdi
	popq %rsi
	popq %rax //ret func addr
	popq %rdx
	popq %rcx
	popq %rbx
	popq %rsp
	pushq %rax
	
	xorl %eax, %eax
	ret

接下来截止到popq %rsp,就是恢复下一个协程的现场了。

第一句是让rsp指向下一个协程控制块保存寄存器组的内存地址。然后不断出栈恢复寄存器。rax保存的是断点地址。最后将rax断点地址压栈,为ret返回断点继续执行做准备。

至于倒数第二句    xorl %eax, %eax  意思是将eax第16位清零(b,w,l,q是操作属性限定符,分别表示1字节,2字节,4字节,8字节),本程序代码没有返回值,所以不是用作返回值。故推测这句话是为了安全考虑。

coctx.cpp大概功能

int coctx_init( coctx_t *ctx );函数负责清零,很简单就不说了。

int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 );

协程上下文初始化

分析文件:https://github.com/Tencent/libco/blob/master/coctx.cpp

贴一遍代码

#include "coctx.h"
#include <string.h>


#define ESP 0
#define EIP 1
#define EAX 2
#define ECX 3
// -----------
#define RSP 0
#define RIP 1
#define RBX 2
#define RDI 3
#define RSI 4

#define RBP 5
#define R12 6
#define R13 7
#define R14 8
#define R15 9
#define RDX 10
#define RCX 11
#define R8 12
#define R9 13


//----- --------
// 32 bit
// | regs[0]: ret |
// | regs[1]: ebx |
// | regs[2]: ecx |
// | regs[3]: edx |
// | regs[4]: edi |
// | regs[5]: esi |
// | regs[6]: ebp |
// | regs[7]: eax |  = esp
enum
{
	kEIP = 0,
	kESP = 7,
};

//-------------
// 64 bit
//low | regs[0]: r15 |
//    | regs[1]: r14 |
//    | regs[2]: r13 |
//    | regs[3]: r12 |
//    | regs[4]: r9  |
//    | regs[5]: r8  | 
//    | regs[6]: rbp |
//    | regs[7]: rdi |
//    | regs[8]: rsi |
//    | regs[9]: ret |  //ret func addr
//    | regs[10]: rdx |
//    | regs[11]: rcx | 
//    | regs[12]: rbx |
//hig | regs[13]: rsp |
enum
{
	kRDI = 7,
	kRSI = 8,
	kRETAddr = 9,
	kRSP = 13,
};

//64 bit
extern "C"
{
	extern void coctx_swap( coctx_t *,coctx_t* ) asm("coctx_swap");
};
#if defined(__i386__)
//略去若干代码
#elif defined(__x86_64__)
int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 )
{
	char *sp = ctx->ss_sp + ctx->ss_size;
	sp = (char*) ((unsigned long)sp & -16LL  );

	memset(ctx->regs, 0, sizeof(ctx->regs));

	ctx->regs[ kRSP ] = sp - 8;

	ctx->regs[ kRETAddr] = (char*)pfn;

	ctx->regs[ kRDI ] = (char*)s;
	ctx->regs[ kRSI ] = (char*)s1;
	return 0;
}

int coctx_init( coctx_t *ctx )
{
	memset( ctx,0,sizeof(*ctx));
	return 0;
}

#endif

主要分析一下coctx_make:见注释

参数1是协程控制块地址。

参数2是,coctx_pfn_t定义如下:

typedef void* (*coctx_pfn_t)( void* s, void* s2 );

表示创建协程后要执行的第一个函数的函数指针,函数包含两个void*类型的参数,返回值为void*

参数3和参数4是向上述pfn传递的两个参数。

int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 )
{
	char *sp = ctx->ss_sp + ctx->ss_size; //计算栈顶地址,这里多加了一个寄存器单位也就是8字节
	sp = (char*) ((unsigned long)sp & -16LL  );//这里对第4位清零,使栈64位内存对齐。向下取整跟满递减搭配。

	memset(ctx->regs, 0, sizeof(ctx->regs));

	ctx->regs[ kRSP ] = sp - 8; //这里把多加的8扣回来,表示栈顶指针
	ctx->regs[ kRETAddr] = (char*)pfn;//将返回地址初始化为用户创建协程时传入的开始函数地址,也就是从函数头开始执行。

	ctx->regs[ kRDI ] = (char*)s; //函数原型设置为两个参数,s和s1即向函数传递的两个参数。
	ctx->regs[ kRSI ] = (char*)s1;
	return 0;
}

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我来改进一下ch07_09.cpp中的冒泡排序算法。冒泡排序算法的主要思想是比较相邻两个元素的大小,如果前一个元素比后一个元素大,就将它们交换位置,这样一轮比较下来,最大的元素就会被移动到数组的末尾。我们可以通过增加一些优化来提高冒泡排序的效率,比如记录上一次交换的位置,下一轮比较时只需要比较到上一次交换的位置,因为该位置之后的元素已经是有序的。 下面是改进后的代码: ```c++ #include <iostream> using namespace std; void bubbleSort(int arr[], int n) { int i, j, lastSwap; for (i = 0; i < n - 1; i++) { lastSwap = n - 1; // 初始化为数组末尾 for (j = 0; j < lastSwap; j++) { if (arr[j] > arr[j + 1]) { swap(arr[j], arr[j + 1]); lastSwap = j; // 更新上一次交换的位置 } } if (lastSwap == n - 1) break; // 如果这一轮没有发生交换,说明数组已经有序,结束循环 } } int main() { int arr[] = {5, 2, 8, 4, 7, 1, 3, 9, 6}; int n = sizeof(arr) / sizeof(arr[0]); bubbleSort(arr, n); cout << "排序后的数组:"; for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; return 0; } ``` 这里我们增加了一个变量`lastSwap`来记录上一次交换的位置,每次内层循环时都将`lastSwap`初始化为数组的末尾。当发生交换时,将`lastSwap`更新为当前交换的位置。下一轮比较时,只需要比较到`lastSwap`即可。如果这一轮没有发生交换,说明数组已经有序,结束循环。这样可以减少比较的次数,提高冒泡排序的效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值