c/c++ setjmp、longjmp实现,实现一个简单的协程

1.任务切换原理

1.1 x86用户层几个关键的寄存器

  • eip/rip:存放cpu下一条指令
  • esp/rsp:存放栈顶地址
  • ebp/rbp:保存函数栈底,用于快速找到函数参数和局部变量

1.2 逆向一个函数了解几个关键的寄存器的作用

#include <stdio.h>

int add(int a,int b)
{
    return a+b;
}

int main(int argc,char *argv[])
{
 	add(2,3);   

    return 0;                                                                             
}
1.2.1 32位汇编
add:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %edx
    movl    12(%ebp), %eax
    addl    %edx, %eax
    popl    %ebp
    ret 
main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   $3  
    pushl   $2  
    call    add 
    addl    $8, %esp
    movl    $0, %eax               
    leave
    ret

了解一些简单的指令(这种汇编格式是左操作数到右操作数)

mov:类似’=’。

push:压栈指令,=》*(esp) = 操作数,esp + 4

pop:弹栈指令,=>寄存器 = *(esp) ,esp-4;

add:类似’+=’;

call:push eip,eip = 操作数。(实现代码跳转,cpu会执行eip寄存器指向的指令)

ret:pop eip;

从"pushl $3"开始画堆栈图

在这里插入图片描述

  • 调用函数前参数从右向左压栈
  • 进入函数中,保存旧的ebp,让ebp指向新函数的栈底.便与找到函数的参数和局部变量(ebp+8=>参一,ebp+12=>参二)
  • 退出函数时,恢复旧的栈底和eip
1.2.2 64位汇编
add:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    popq    %rbp
    ret 
main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movl    %edi, -4(%rbp)
    movq    %rsi, -16(%rbp)
    movl    $3, %esi
    movl    $2, %edi
    call    add                                                          	 
    movl    $0, %eax
    leave
    ret

和32位没什么太大区别,只是调用时不在压栈传参

寄存器传参顺序为edi,esi,edx,ecx,r8d,r9d,超过6个参数和32位一样使用栈传参

1.3 切换实现

一个程序运行到哪里关键时eip寄存器保存的值,只要可以改变eip的值程序可以运行到任何想要跑的地方。但是直接改变eip的值不考虑堆栈,程序将混乱。所以程序切换时要保存堆栈,eip和一些通用寄存器的值,等到切换回来时再给寄存器恢复。

一个任务的体现就是一个函数,到某地方不想运行时保存一组寄存器的值,切换到另一个任务

在这里插入图片描述

切换代码实现(64位)

  • task_ctx的定义
struct task_ctx
{
	unsigned long eax;
	unsigned long ebx;
	unsigned long ecx;
	unsigned long edx;
	unsigned long edi;
	unsigned long esi;
	unsigned long esp;
	unsigned long ebp;
	unsigned long eip;
};

这个结构体保存一些需要保存的寄存器的值。

  • 切换函数
static void Switch(struct task_ctx *src,struct task_ctx *obj)
{
	asm volatile(
	"movq %%rax,0(%%rdi)\n\t"	//save eax  --- 将cpu的值保存到src
	"movq %%rbx,8(%%rdi)\n\t"	//save ebx
	"movq %%rcx,16(%%rdi)\n\t"	//save ecx
	"movq %%rdx,24(%%rdi)\n\t"	//save edx
	"movq %%rdi,32(%%rdi)\n\t"	//save edi
	"movq %%rsi,40(%%rdi)\n\t"	//save esi
	"movq %%rbp,%%rbx\n\t"
	"add $16,%%rbx\n\t"			//rbx = esp
	"movq %%rbx,48(%%rdi)\n\t"  //save esp
	"movq 0(%%rbp),%%rbx\n\t"
	"movq %%rbx,56(%%rdi)\n\t"	//save ebp
	"movq 8(%%rbp),%%rbx\n\t"
	"movq %%rbx,64(%%rdi)\n\t"  //save eip
	"movq 0(%%rsi),%%rax\n\t"	//restore eax --- 将obj的值恢复到cpu
	"movq 16(%%rsi),%%rcx\n\t"	//restore ecx
	"movq 24(%%rsi),%%rdx\n\t"	//restore edx
	"movq 48(%%rsi),%%rsp\n\t"	//restore esp
	"movq 56(%%rsi),%%rbp\n\t"	//restore ebp --- 此时堆栈已经切换,如果没有分配内存会崩溃
	"movq 64(%%rsi),%%rbx\n\t"	//rbx = eip
	"pushq %%rbx\n\t"			//push eip
	"movq 8(%%rsi),%%rbx\n\t"	//restore ebx
	"movq 32(%%rsi),%%rdi\n\t"	//restore edi
	"movq 40(%%rsi),%%rsi\n\t"	//restore rsi
	"ret\n\t"					//pop eip     --- 程序将运行再obj->eip
	:
	:);
}

c语言无法直接改变寄存器的值,所以使用嵌入汇编的形式,我们保存的状态时调用Switch后的状态。

在这里插入图片描述

大部分汇编代码只要mov就行,再调用函数时堆栈会改变我们必须保存旧的值

	"movq %%rbp,%%rbx\n\t"
    "add $16,%%rbx\n\t"			//rbx = 旧的esp
    "movq %%rbx,48(%%rdi)\n\t"  //save esp
    "movq 0(%%rbp),%%rbx\n\t"	//rbx = 旧的rbp
    "movq %%rbx,56(%%rdi)\n\t"	//save ebp
    "movq 8(%%rbp),%%rbx\n\t"	//rbx = rip 这里的rip就是调用switch的下一条指令的值
    "movq %%rbx,64(%%rdi)\n\t"  //save eip

这个函数的上半部分可以理解为setjmp,后半部分可以理解为longjmp。

1.3.1 setjmp和longjmp的实现
struct task_ctx
{
	unsigned long eax;
	unsigned long ebx;
	unsigned long ecx;
	unsigned long edx;
	unsigned long edi;
	unsigned long esi;
	unsigned long esp;
	unsigned long ebp;
	unsigned long eip;
};

void my_setjmp(struct task_ctx *ctx)
{
	asm volatile(
			"movq %%rax,0(%%rdi)\n\t"
			"movq %%rbx,8(%%rdi)\n\t"
			"movq %%rcx,16(%%rdi)\n\t"
			"movq %%rdx,24(%%rdi)\n\t"
			"movq %%rdi,32(%%rdi)\n\t"
			"movq %%rsi,40(%%rdi)\n\t"
			"movq %%rbp,%%rbx\n\t"
			"add $16,%%rbx\n\t"
			"movq %%rbx,48(%%rdi)\n\t"  //save esp
			"movq 0(%%rbp),%%rbx\n\t"
			"movq %%rbx,56(%%rdi)\n\t"	//save ebp
			"movq 8(%%rbp),%%rbx\n\t"
			"movq %%rbx,64(%%rdi)\n\t"  //save eip
			:
			:);
}

void my_longjmp(struct task_ctx *ctx)
{
	asm volatile(
			"movq 0(%%rdi),%%rax\n\t"	
			"movq 16(%%rdi),%%rcx\n\t"
			"movq 24(%%rdi),%%rdx\n\t"
			"movq 48(%%rdi),%%rsp\n\t"
			"movq 56(%%rdi),%%rbp\n\t"
			"movq 64(%%rdi),%%rbx\n\t"
			"pushq %%rbx\n\t"			//push eip
			"movq 8(%%rdi),%%rbx\n\t"
			"movq 32(%%rdi),%%rdi\n\t"
			"movq 40(%%rdi),%%rsi\n\t"
			"ret\n\t"					//pop eip
			:
			:);
}

struct task_ctx ctx = {0};

void test()
{
	printf("----a----\n");
	my_longjmp(&ctx);
}

int main(int argc,char *argv[])
{
	my_setjmp(&ctx);
	test();

	return 0;
}

1.4 测试切换代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct task_ctx
{
	unsigned long eax;
	unsigned long ebx;
	unsigned long ecx;
	unsigned long edx;
	unsigned long edi;
	unsigned long esi;
	unsigned long esp;
	unsigned long ebp;
	unsigned long eip;
};

static void Switch(struct task_ctx *src,struct task_ctx *obj)
{
	asm volatile(
			"movq %%rax,0(%%rdi)\n\t"
			"movq %%rbx,8(%%rdi)\n\t"
			"movq %%rcx,16(%%rdi)\n\t"
			"movq %%rdx,24(%%rdi)\n\t"
			"movq %%rdi,32(%%rdi)\n\t"
			"movq %%rsi,40(%%rdi)\n\t"
			"movq %%rbp,%%rbx\n\t"
			"add $16,%%rbx\n\t"
			"movq %%rbx,48(%%rdi)\n\t"  //save esp
			"movq 0(%%rbp),%%rbx\n\t"
			"movq %%rbx,56(%%rdi)\n\t"	//save ebp
			"movq 8(%%rbp),%%rbx\n\t"
			"movq %%rbx,64(%%rdi)\n\t"  //save eip
			"movq 0(%%rsi),%%rax\n\t"	
			"movq 16(%%rsi),%%rcx\n\t"
			"movq 24(%%rsi),%%rdx\n\t"
			"movq 48(%%rsi),%%rsp\n\t"
			"movq 56(%%rsi),%%rbp\n\t"
			"movq 64(%%rsi),%%rbx\n\t"
			"pushq %%rbx\n\t"			//push eip
			"movq 8(%%rsi),%%rbx\n\t"
			"movq 32(%%rsi),%%rdi\n\t"
			"movq 40(%%rsi),%%rsi\n\t"
			"ret\n\t"					//pop eip
			:
			:);
}

struct task_ctx src;
struct task_ctx obj;

void printf_a()
{
	printf("function start\n");
	printf("aaaaaaa\n");
	printf("function end\n");
	Switch(&obj,&src);
}

int main(int argc,char *argv[])
{
    //分配栈内存
	unsigned long ptr = (unsigned long)malloc(4096);
	if(ptr == 0){
		printf("malloc error\n");
		return -1;
	}
	//压栈是从高地址向低地址
	obj.esp = ptr + 4096;
	obj.ebp = obj.esp;
	obj.eip = (unsigned long)printf_a;//设置切换地址

	Switch(&src,&obj);

	printf("------main end------\n");

	return 0;
}

输出结果:

function start
aaaaaaa
function end
------main end------

我们希望协程结束时,去执行一些收尾的工作,在执行完任务函数时去执行收尾函数,所以可以把代码改下

void task_exit()
{
    printf("\n----task_exit----\n");
    while(1);
}

int main(int argc,char *argv[])
{
    unsigned long ptr = (unsigned long)malloc(4096);
    if(ptr == 0){ 
        printf("malloc error\n");
        return -1; 
    }   

    *((unsigned long *)(ptr + 4088)) = (unsigned long)task_exit;
    obj.esp = ptr + 4088;
    obj.ebp = obj.esp;
    obj.eip = (unsigned long)printf_a;

    Switch(&src,&obj);
	
    return 0;
}

输出结果:

function start
aaaaaaa
function end

----task_exit----

函数最后会执行’ret’指令,所以我们可以改变堆栈的值去让任务结束时执行收尾函数。

切换还可以使用:

  • setjmp、longjmp
  • ucontext

2. 协程实现

2.1 协程设计

协程最终体现就是一个函数(任务),我们为这个函数对应一个上下文,也就是之前实现的‘struct task_ctx’。当这个函数不运行时保存这个函数的状态(一组寄存器的值),切换到下一个函数(任务)。我们使用定时器让程序去切换任务。

在这里插入图片描述

2.2 协程定义

#define TASK_RUN   1
#define TASK_END   2

struct task
{
	int state;						//状态
	int pid;						//协程id
	void *start_addr;				//堆栈起始地址
	rbtree_node pid_node;			//红黑树节点
	queue_node ready_node;			//就绪队列节点
	queue_node end_node;			//死亡队列节点
	struct task_ctx ctx;			//上下文
	void (*start_routine)(void *);	//函数
	void *arg;						//参数
};

#define GET_STRUCT_MEMBER_ADDR(type,member) (unsigned long)(&((type *)0)->member)
//使用这个宏去得到结构体的首地址 (类型,成员名,成员地址)
#define GET_STRUCT_START_ADDR(type,member,member_addr) (void *)((char *)member_addr - GET_STRUCT_MEMBER_ADDR(type,member))

2.3 协程初始化

struct task *p = (struct task *)calloc(1,sizeof(struct task));

p->pid = index++;
p->start_routine = start_routine;
p->arg = arg;
p->state = TASK_RUN;

p->ctx.eip = (unsigned long)start_routine; //函数起始地址
p->ctx.edi = (unsigned long)arg;			//设置参数(64位函数调用方式,rdi为参数一)

p->start_addr = calloc(1,10240);		//申请栈空间1M

p->ctx.esp = (unsigned long)p->start_addr + 10232;
*((unsigned long *)p->ctx.esp) = (unsigned long)task_exit;//设置协程退出函数
p->ctx.ebp = p->ctx.esp;

2.4 协程调度

在这里插入图片描述

源码地址:https://github.com/huoyang11/userpro

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值