如何在用户态完成一次上下文切换
X64栈幁结构
-
X64总体的栈幁总体如下图
-
GCC没有优化的情况下的反编译
栈幁模型详述
-
读者通过上图观察可以看到函数的第一个参数放在RDI寄存器,第二个参数放在RSI寄存器 ... 后续参数放在何处,相关调用约定可以查看AMD 64 调用约定
-
读者从上图可以看出 ,在汇编层面上,由函数的被调用方保存 rsp rbp 指针, 函数a b c 在汇编层面都是首先将 rbp入栈(push rbp) 然后对rsp操作 (例如sub rsp , 16;)并都会在函数返回前分别 将rbp出栈 (pop rbp) 还原rsp (例如 add rsp, 16;)
-
另外X86864 fastcall约定使用RAX寄存器保存返回值
总结一下汇编的函数的模板大致如下
- 保护当前帧的调用方的rbp寄存器
- 将当前的rsp指针作为当前帧的rbp指针,防止破坏当前调用方帧的临时变量
- 将rsp指针减去固定的数量 并存回rsp寄存器, 防止接下来当前帧中的被调用方破坏当前帧中的临时变量
-
返回前将当前帧需要返回的结果写入RAX寄存器,这一步比较隐晦 因为只有函数c 出现了这个步骤,在实际中函数返回都会依照约定写入RAX寄存器,由于函数b本身并没有对函数返回结果(RAX寄存器的值)进行加工,因此函数b返回的时候RAX寄存器的值并不变化,同理a也是如此。
-
分别恢复rsp rbp寄存器 ,如下图中 add rsp, 16 跟 pop rbp
通过上面几个套路模板,基本上我们就能使用汇编来编写汇编函数了,然后记住要非常小心地操作寄存器,避免破坏调用方的栈幁(函数中的临时变量)。
如何在用户态完成一次上下文切换
-
X64下一个线程在运行的时候,有一个PC寄存器指向当前线程的汇编代码的位置,我们需要通过更换PC寄存器中的值 让CPU接下来从PC寄存器中新的位置运行汇编代码
-
另外我们希望 从当前的上下文切换到另外一个上下文后,CPU能够切换回来继续正常运行,根据前面所说,那么我们需要保护当前上下文的 RSP RBP 指针,并且保证当前上下文的整个栈幁的区域不会被另外一个上下文给破坏
切换的具体步骤
参考 我之前写的 JVM暂停工作线程机制
- 注册信号,让操作系统 接下来切换上下文到 handler函数
//全局变量
char *buffer;
int pagesize;
void allocate_memory() {
/*
* 初始化信号量结构体
*/
struct sigaction sa;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
// 注册 handler 函数 ,当我们触发信号的时候 操作系统会将切换到 handler 这个函数上下文来运行代码
sa.sa_sigaction = handler;
if (sigaction(SIGSEGV, &sa, NULL) == -1)
handle_error("sigaction");
pagesize = sysconf(_SC_PAGE_SIZE);
if (pagesize == -1)
handle_error("sysconf");
// 初始化buffer指针指向的内存区域
buffer = memalign(pagesize, 4 * pagesize);
if (buffer == NULL)
handle_error("memalign");
printf("Start of region: 0x%lx\n", (long) buffer);
//设置buffer指针为只读,接下来如果访问到buffer指针指向的内存区域就会触发信号
if (mprotect(buffer, pagesize * 4,
PROT_READ) == -1)
handle_error("mprotect");
}
- 触发操作系统信号前 做一些准备工作
int main(int argc, char *argv[]) {
/*
* allocate memory and set memory access READ
*/
allocate_memory();
char *p = buffer;
// 初始化当前函数调用帧中的3个临时变量
uint64 local_pc = 0;
uint64 local_rsp = 0;
uint64 local_rbp = 0;
//以下汇编代码是我根据C语言反编译后确定 三个local_变量的位置
__asm__(".intel_syntax;"
// 将当前CPU运行的代码的位置写入 RAX寄存器 方便handler中切换回来
// 其实这个rax寄存器中最终存放的内存地址 也就是 lea rax, [rip] 这个汇编代码在内存中的位置。
"lea rax, [rip];"
//保存RAX寄存器 到 local_pc变量
"mov [rbp-0x20], rax;"
//保存RSP寄存器 到 local_rsp变量
"mov [rbp-0x28], rsp;"
//保存RBP寄存器 到 local_rbp变量
"mov [rbp-0x30], rbp;"
);
//写入全局变量
pc = local_pc;
rsp = local_rsp;
rbp = local_rbp;
for (int i = 0; i < 4; i++) {
//当我们操作p指针也就是buffer指针的时候,就会触发信号
*(p) = 'a';
p++;
}
*(p) = '\0';
printf("p = %s\n", buffer);
//printf("%x",local_pc);
/*
* if we didn't restore those registers,
* it should not happen.
*/
printf("Loop completed\n");
exit(EXIT_SUCCESS);
}
- 信号触发后
static void handler(int sig, siginfo_t *si, void *unused) {
/*
* 打印受保护的内存地址
*/
printf("Got SIGSEGV at address: 0x%lx\n",
(long) si->si_addr);
// 取消buffer指针指向内存区域的内存保护权限
if (mprotect(buffer, pagesize * 4,
PROT_READ | PROT_WRITE) == -1)
handle_error("mprotect");
// 此时线程的控制权 已经归JVM代码掌控,JVM可以挂起当前线程,等完成GC垃圾回收工作后再恢复状态
// 老办法 将之前全局变量保存的寄存器值恢复到当前函数帧中的临时变量
uint64 local_rsp = rsp;
uint64 local_pc = pc;
uint64 local_rbp = rbp;
// 具体三个 local_xxx 变量的地址,依旧是我通过反编译C程序确定的
// 还是老办法 该怎么写到全局变量的 又怎么通过本地变量 local_xxx 写回到寄存器里面去
__asm__(".intel_syntax;"
"mov rsp,[rbp-0x20];"
"mov rax,[rbp-0x28];"
"mov rbp,[rbp-0x30];"
// 此处非常关键 直接jmp 调回去 让CPU回到之前main函数的上下文继续执行代码
"jmp rax;"
);
//never happen
printf("rsp:%x", local_rsp);
printf("pc:%x", local_pc);
printf("rbp:%x", local_rbp);
exit(EXIT_FAILURE);
}
整个流程的DEBUG的GIF如下
联系我
- 如果有不懂的地方,欢迎通过留言联系我