为什么我不遵循异常处理程序中的寄存器保存约定?



在我之前的问题中,我在程序集中发布了这段代码(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的指导方针,但我就是不明白为什么?

但是:

  1. 这些寄存器是调用方保存的,我在调用funciton_call并再次加载它们之前保存它们,这有什么问题?

  2. 在调用不是函数调用的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调用约定的函数


例如,安全代码可能是这样的。(未经测试(。将RIP传递给what_to_do并让它返回新的RIP或0来运行旧的处理程序可能更有意义。(作为奖励,您不需要在该函数调用中保存任何额外的状态,只需要保存用户空间状态即可。(

x86指令在操作码之后有一个可变的字节数,这取决于寻址模式和立即数,所以只将用户空间RIP增加1或2是没有意义的。或者,如果第一个字节实际上是像reprex这样的前缀。。。

您可能有长度超过2字节的非法指令,例如使用寄存器源(REX+操作码+modrm(编码的lea。或者66 66 0F 0B(UD2前面有两个前缀(。因此,当函数只关注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.

最新更新