assembly - 为什么我不遵循异常处理程序中的寄存器保存约定?
问题描述
在我之前的问题中,我在程序集中发布了这段代码(x86-64 att),它替换了无效操作码的处理程序(或者如果what_to_do
函数返回 0,则可以调用前一个):
.globl my_ili_handler
.text
.align 4, 0x90
my_ili_handler:
movq (%rsp), %r8 # loading %rip from stack
movb (%r8), %dil # reading first byte in the invalid opcode
cmpb $0x0F, %dil
jne function_call
movb 1(%r8), %dil # else read the 2nd byte instead
addq $1, %r8
function_call:
addq $1, %r8
pushq %rbp # save old %rbp
movq %rsp, %rbp # move %rbp to top
# %rax, %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11 caller saved.
subq $8, %rsp
pushq %r8 # backup %r8
call what_to_do # unsigned int what_to_do(unsigned char magic)
popq %r8 # restore %r8
leave # push return address and jump
cmpl $0, %eax
je old_handler
mov %eax, %edi # zero the upper part of %rdi
addq $8, %rsp # pop old %rip from stack
pushq %r8
jmp end
old_handler:
jmp *old_ili_handler(%rip)
end:
iretq # go back to user space
你们中的许多人指出我没有遵循有关保存 r8 和 rdx 的指南,但我就是不明白为什么会这样?
但:
这些寄存器是调用者保存的,我在调用
funciton_call
并再次加载它们之前保存它们,那有什么问题?在调用不是函数调用的 jmp 之前,我不需要保存它们......
加上我应该如何在不破坏我的整个代码的情况下解决这个问题?
第一次编辑:
.globl my_ili_handler
.text
.align 4, 0x90
my_ili_handler:
movq (%rsp), %r8 # loading %rip from stack
movb (%r8), %dil # reading first byte in the invalid opcode
cmpb $0x0F, %dil
jne function_call
movb 1(%r8), %dil # else read the 2nd byte instead
addq $1, %r8
function_call:
addq $1, %r8
pushq %rbp # save old %rbp
movq %rsp, %rbp # move %rbp to top
# %rax, %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11 caller saved.
subq $72, %rsp
# backup all caller-saved registers
pushq %rax
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %r8
pushq %r9
pushq %r10
pushq %r11
call what_to_do # unsigned int what_to_do(unsigned char magic)
# restore all caller-saved registers
popq %r11
popq %r10
popq %r9
popq %r8
popq %rcx
popq %rdx
popq %rsi
popq %rdi
popq %rax
leave # (mov %rbp, %rsp) & (pop %rbp)
cmpl $0, %eax
je old_handler
mov %eax, %edi # zero the upper part of %rdi
addq $8, %rsp # pop old %rip from stack
pushq %r8
jmp end
old_handler:
jmp *old_ili_handler(%rip)
end:
iretq # go back to user space
解决方案
您的中断处理程序不是函数。寄存器的整个传入状态(除了 RSP 和 RFLAGS)属于用户空间。
jmp *old_ili_handler(%rip)
最终会在故障发生时记录用户空间的状态,因此您要避免扭曲信号处理程序或核心转储看到的用户空间状态。
您可以查看所有寄存器都是旧处理程序尾部调用的“参数”。 (它也是一个中断处理程序,而不是一个函数,因此您jmp
可以使用堆栈/寄存器来处理它,该状态与您的处理程序进入时的状态相匹配,因此它的工作方式就好像它是直接从用户空间中的故障中调用的一样。)
请注意与类似函数的相似性int foo(int x){return bar(x);}
,它将编译为jmp bar
而不是call bar
/ ret
。即一个优化的尾调用,它只将 args 留在寄存器中。但同样,对于可以返回、传递信号或触发核心转储的异常处理程序,所有寄存器中的整个用户空间状态实际上是一个参数。
一般来说,对于其他错误,例如页面错误,可以在修复问题后恢复用户空间,更重要的是不要破坏寄存器:而不是仅仅将错误信息输入核心转储(或破坏模拟丢失指令的偶尔程序通过 SIGILL 处理程序),add (%r8), %edi
如果最终返回用户空间具有不同的寄存器值,您将中断代码。实际上,您的代码现在有时会跳转到,iret
因此您会直接返回用户空间以重试错误指令,也许是在修复它之后,所以您确实遇到了这个问题。
请注意,您实际上应该保存/恢复您周围的所有调用破坏寄存器call what_to_do
,因为它是一个遵循 C 调用约定的函数。
例如,安全代码可能看起来像这样。(未经测试)。what_to_do
将 RIP 传递给并让它返回新的 RIP 或0
运行旧的处理程序可能更有意义。(作为奖励,您不需要在该函数调用中保存任何额外的状态,只需保存用户空间状态。)
x86 指令在操作码之后的字节数可变,具体取决于寻址模式和立即数,因此仅将用户空间 RIP 增加 1 或 2 是没有意义的。或者如果第一个字节实际上是前缀,例如rep
或rex
...
您可以拥有超过 2 个字节的非法指令,例如lea
使用寄存器源编码的指令(REX + opcode + modrm)。或 a 66 66 0F 0B
(UD2 前面的 2 个前缀)。因此,当它仅查看 1 个字节时,可能会混淆您的函数。
但无论如何,我保留了您的原始指令长度解码,以显示使用保留调用的寄存器来记住整个调用中的某些内容,与保存用户空间的状态分开。
.globl my_ili_handler
.text
.p2align 4
my_ili_handler:
push %rbx # save a call-preserved reg for our own use
# %rax, %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11 are call-clobbered
push %rax
push %rcx
push %rdx
push %rsi
push %rdi
push %r8
push %r9
push %r10
push %r11
mov 10*8(%rsp), %rbx # loading user-space fault address from exception frame
# note the n*8(%rsp) since this is after n pushes; same address as (%rsp) on entry.
movzbl (%rbx), %edi # byte load of the invalid opcode
inc %rbx
cmp $0x0F, %edi # check for 2-byte opcode escape byte
jne function_call
inc %rbx
movzbl (%rbx), %edi # else read the 2nd byte instead
function_call:
# RBX points to fault-address + 1 or 2 depending on seeing 0F.
# Very primitive instruction-length decoding that ignores prefixes
# and illegal forms of longer instructions with ModRM and/or immediate operands
# subq $8, %rsp # 16-byte stack alignment probably not needed in kernel, and I didn't check what the initial alignment was on entry vs. the number of pushes
cld # C calling convention requires DF=0, user-space might have left DF=1
# 64-bit mode can I think avoid worrying about DS and ES settings
call what_to_do # unsigned int what_to_do(unsigned char magic)
cmpl $0, %eax
# now restore everything, before we either
# run the old handler transparently or return to user-space with its regs unchanged
pop %r11
pop %r10
pop %r9
pop %r8
pop %rdi
pop %rsi
pop %rdx
pop %rcx
pop %rax
je run_old_handler
end:
# mov %eax, %edi # zero the upper part of %rdi.
# IDK what this was for. Is user-space supposed to get this return value?
# If so, only restore RAX in the other path instead of before the branch
# and add $8, %rsp here instead.
mov %rbx, 8(%rsp) # set the user-space RIP
pop %rbx # restore our call-preserved register
iretq # and return to user-space at the updated RIP
run_old_handler:
pop %rbx # just restore RBX
jmp *old_ili_handler(%rip) # and run the old handler with all registers in identical state to entry to this handler.
推荐阅读
- c++ - Using map function to map to user menu options to specific functions
- c++ - How to take vector as input in C++?
- flutter - 例外:无法构建插件analog_clock
- google-cloud-platform - Google Cloud Healthcare API 不支持 PlanDefintion/$apply 和 Activity Definition/$apply
- c# - c#如何在win10中通过右键将选定的文本添加到我的程序中
- javascript - 试图从 Mongo 读取数据
- javascript - date-fns:如何在应用程序范围内定义默认语言环境?
- c - SSL_free 函数崩溃
- python - 与 Chrome 浏览器类似,如何使用 Python 从 PDF 中提取文本?
- android - 无法使用 Flutter 和 Firebase 设置 Facebook 身份验证