文章目录
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