如何保存x86_64中断服务程序的寄存器?

29

我正在查看一些来自学校项目的旧代码,在我的笔记本上尝试编译时遇到了一些问题。它最初是为旧版本的32位gcc编写的。无论如何,我试图将一些汇编代码转换为64位兼容代码,并遇到了一些问题。

这里是原始代码:

pusha
pushl   %ds
pushl   %es
pushl   %fs
pushl   %gs
pushl   %ss

pusha在64位模式下无效。那么,在64位模式下,使用x86_64汇编的正确方法是什么?

肯定有原因使pusha在64位模式下无效,所以我觉得手动推送所有寄存器可能不是一个好主意。

5个回答

27

当AMD开发64位x86扩展时,他们需要一些空间来添加新的REX前缀和一些其他新指令。他们将某些操作码的含义改为这些新指令。

其中有几个指令只是现有指令的简写形式,或者不必要。 PUSHA 就是其中之一。但目前并不清楚为什么禁用了 PUSHA,因为它似乎没有重叠任何新指令操作码。也许他们保留了 PUSHAPOPA 操作码以备未来使用,因为它们是完全冗余的,在代码中不会频繁出现也不会更快。

PUSHA 的顺序是指令编码的顺序:eaxecxedxebxespebpesiedi。请注意,它冗余地推送了 esp!您需要知道 esp 才能找到它推送的数据!

如果您正在从64位转换代码,则 PUSHA 代码无效,您需要将其更新为推送新寄存器 r8r15。您还需要保存和恢复更大的SSE状态,xmm8xmm15。假设您将要清除它们。

如果中断处理程序代码只是将控制权转发给C代码的存根,你不需要保存所有寄存器。你可以假设C编译器将生成代码来保留rbxrbprsirdir12r15。你只需要保存和恢复raxrcxrdx以及r8r11(注意: 在Linux或其他System V ABI平台上,编译器将保留rbxrbpr12-r15,你可以期望rsirdi被破坏)
在长模式下,段寄存器没有值 (如果被中断的线程运行在32位兼容模式下,你必须保留段寄存器,感谢ughoavgfhw)。实际上,在长模式下,大部分分段已经被取消了,但FS仍然为操作系统保留,用作线程本地数据的基地址。寄存器值本身并不重要,FSGS的基址通过MSR 0xC00001000xC0000101设置。假设你不会使用FS,你不需要担心它,只需记住由C代码访问的任何线程本地数据都可能使用任意随机线程的TLS。要小心这一点,因为C运行时库使用TLS来执行某些功能 (例如:strtok通常使用TLS)。

将一个值加载到FSGS中(即使在用户模式下),都会覆盖FSBASEGSBASE MSR。由于一些操作系统使用GS作为“处理器本地”存储(它们需要一种方法来为每个CPU拥有一个指向结构体的指针),所以它们需要将其保留在不会被加载到GS的位置。为了解决这个问题,有两个MSR保留给GSBASE寄存器:一个是活跃的,一个是隐藏的。在内核模式下,内核的GSBASE保存在通常的GSBASE MSR中,而用户模式基址则保存在另一个(隐藏的)GSBASE MSR中。从内核模式切换到用户模式上下文时,并且保存用户模式上下文并进入内核模式时,上下文切换代码必须执行SWAPGS指令,该指令交换可见和隐藏的GSBASE MSR的值。由于内核的GSBASE在用户模式下安全地隐藏在另一个MSR中,因此用户模式代码无法通过将值加载到GS中来破坏内核的GSBASE。当CPU重新进入内核模式时,上下文保存代码将执行SWAPGS并恢复内核的GSBASE


1
我猜AMD不想为一个保存两倍宽度的寄存器数量的64位pusha编写新的微码。有趣的是,AMD k7和k8在32位模式下的pusha速度是8个连续的push指令的两倍(每4个时钟周期一个,根据Agner Fog的测试:https://agner.org/optimize/),因此显然它一次存储一对32位寄存器以绕过每个时钟周期1个存储的瓶颈。在英特尔上,它不值得用于性能,只用于代码大小(Merom上的8c吞吐量,18个uops,Intel的第一个64位P6微架构)。 - Peter Cordes
1
RIP POPA... (1978-2000) - Dima Rich

11

学习已有的完成此类工作的代码,例如:

实际上,在 AMD64 上,“手动推送”寄存器是唯一的方法,因为没有 PUSHA。AMD64 在这个方面并不独特-大多数非 x86 CPU 在某些时候也需要逐个寄存器的保存和恢复。

但是,如果您仔细检查所引用的源代码,您会发现并非所有中断处理程序都需要保存/恢复整个寄存器集,因此存在优化的空间。


1
我在维基百科的文章上读到,x86-64与x86向后兼容。去掉一条指令会不会破坏这种兼容性呢? - Ciro Santilli OurBigBook.com
前两个链接已经失效。 - qwr
@qwr 我已经将第一个链接替换为指向宏被移除之前提交的链接。 - Drew McGowen

9

pusha 在64位模式下无效,因为它是多余的。逐个推送每个寄存器才是正确的做法。


4
在64位模式下,你不能直接推送段寄存器。你需要先将它们复制到另一个寄存器中。mov %ds,%eax; push %rax - ughoavgfhw
@ughoavgfhw 在长模式下,段寄存器不保存任何有意义的值。所有段的基址都为零且没有限制,除了FS和GS之外,它们的基地址由MSR 0xC0000100和0xC0000101控制,旨在用作方便的线程本地存储指针。 - doug65536
2
@doug65536 有时仍然需要保留它们。例如,运行32位程序的64位操作系统在收到中断时需要保存段寄存器,因为该程序使用分段。 - ughoavgfhw
1
好主意。我应该说,如果你正在运行纯64位代码,你可以忽略段寄存器。 - doug65536
1
鉴于引入了可从用户模式访问的wrgsbase和wrfsbase指令,如果操作系统希望允许应用程序更改段(以供其他用途),则保存gs和fs基址可能是必要的。@doug65536 - Ajay Brahmakshatriya

6

嗨,这可能不是正确的方法,但可以创建宏,例如:

.macro pushaq
    push %rax
    push %rcx
    push %rdx
    push %rbx
    push %rbp
    push %rsi
    push %rdi
.endm # pushaq

并且

.macro popaq
    pop %rdi
    pop %rsi
    pop %rbp
    pop %rbx
    pop %rdx
    pop %rcx
    pop %rax
.endm # popaq

如果需要,最终还可以添加其他R8-15寄存器。


如果您正在调用由C编译器生成的代码,则它将已经保留了RBP和RBX(以及R12-R15),假设是x86-64 System V调用约定。如果您要省略一些寄存器的保存/恢复,r8-r15不是一个好选择!这有点类似于pusha/popa,但(真正的pusha存储ESP,而popa跳过间隙。但这很少有用,因此可以省略。) - Peter Cordes

0
拿一个短程序做为例,今天我在测试的时候,我想要做同样的事情,在开始执行我们刚学到的系统调用之前备份所有寄存器。所以我首先尝试了 pusha 和 popa 这个我在旧的 IA-32 Intel 架构软件开发手册中找到的方法。然而它并没有起作用。我已经手动测试过这个方法是可行的,但是出现了一些问题。
#Author: Jonathan Lee
#Professor: Devin Cook
#CSC35
#4-27-23
#practicesyscall.asm

.intel_syntax noprefix

.data

Message:
        .asciz "Learning about system calls.\n"

.text

.global _start

_start:
        pusha   #facilitates saving the current general purpose registers only 32 bit processor
        mov rax, 1
        mov rdi, 1
        lea rsi, Message
        mov rdx, 30
        syscall
        popa    #facilitates restoring the registers only 32 bit processor
        mov rax, 60
        mov rdi, 0
        syscall

当使用x64编译时,这是结果:

practicesyscall.asm: Assembler messages:
practicesyscall.asm:19: Error: `pusha' is not supported in 64-bit mode
practicesyscall.asm:25: Error: `popa' is not supported in 64-bit mode

没有 pusha 和 popa 助记符,它可以工作,这是结果:

Learning about system calls.

这将适用于x32模式: Intel Ref Document 但是,如果您想尝试此方法,它也可以手动工作。
#Jonathan Lee
#CSC35
#Professor Cook
#3-31-23
#Practice NON credit assignment
.intel_syntax noprefix
.data

Intro:
        .ascii "Hello world my name is Jonathan Lee\n\n                                     SAC STATE STINGERS UP\n\n"
        .ascii "This program is to aid in learning more about stacks inside of assembly language.\n"
        .ascii "Register RSP is used to point to the top of the stack. If you use (push) it will load the stack and move the pointer (RSP) automatically.\n"
        .ascii "The stack counts down not up in assembly language.\n"
        .ascii "This is the current RSP point prior to loading a stack or program run:--------------------->  \0"

NewLine:
        .ascii "\n\0"

StackLoad:
    .ascii "Program will now load 1-14 into registers and push to the stack after it will write the register values and current RSP pointer value again.\n"
        .ascii "This will not use registers RSP/RDI they are in use for stack pointer and class subroutines.\n\0"

ZeroLoad:
    .ascii "Program will now load 0 into all registers and write register values after to show values are now stored in memory not registers.\n\0"

PopLoad:
        .ascii "Note RSP is still using the same value. The program will now pop the stack and reverse load the values into the registers, after will write register values.\n"
        .ascii "Last In First Out.\n\0"

RSPAddress:
    .ascii "This is the current next available RSP Pointer Memory Address on top of stack:------------->  \0"

PostStack:
    .ascii "This is the the current next available RSP Pointer Memory Address after stack is popped:--->  \0"

PreExit:
        .ascii "\nNote that the RSP is now pointing to the same memory address value as when the program started.\n\n\0"

.text
.global _start

_start:
        call ClearScreen
        lea rdi, NewLine
        call WriteString
        lea rdi, Intro
        call WriteString
        mov rdi, rsp
        call WriteHex
        lea rdi, NewLine
        call WriteString

        mov rax, 1
        mov rbx, 2
        mov rcx, 3
        mov rdx, 4
        mov rsi, 5
        mov rdi, 6
        mov rbp, 7
        #rsp index use
        mov r8, 8
        mov r9, 9
        mov r10, 10
        mov r11, 11
        mov r12, 12
        mov r13, 13
        mov r14, 14
        mov r15, 15

        lea rdi, StackLoad
        call WriteString
        push rax
        push rbx
        push rcx
        push rdx
        push rsi
        push rbp
        push r8
        push r9
        push r10
        push r11
        push r12
        push r13
        push r14
        push r15

        call WriteRegisters
        lea rdi, RSPAddress
        call WriteString
        mov rdi, rsp
        call WriteHex
        lea rdi, NewLine
        call WriteString
        lea rdi, ZeroLoad
        call WriteString

        mov rax, 0
        mov rbx, 0
        mov rcx, 0
        mov rdx, 0
        mov rsi, 0
        mov rbp, 0
        mov r8, 0
        mov r9, 0
        mov r10, 0
        mov r11, 0
        mov r12, 0
        mov r13, 0
        mov r14, 0
        mov r15, 0

        call WriteRegisterslea rdi, RSPAddress
        call WriteString
        mov rdi, rsp
        call WriteHex
        lea rdi, NewLine
        call WriteString

        lea rdi, PopLoad
        call WriteString


        pop rax
        pop rbx #Last In First Out
        pop rcx
        pop rdx
        pop rsi
        pop rbp
        pop r8
        pop r9
        pop r10
        pop r11
        pop r12
        pop r13
        pop r14#flip stack
        pop r15

        call WriteRegisters
        lea rdi, PostStack
        call WriteString
        mov rdi, rsp
        call WriteHex
        lea rdi, NewLine
        call WriteString
        lea rdi, PreExit
        call WriteString

        call Exit

结果将会是:

Hello world my name is Jonathan Lee

                                     SAC STATE STINGERS UP

This program is to aid in learning more about stacks inside of assembly language.
Register RSP is used to point to the top of the stack. If you use (push) it will load the stack and move the pointer (RSP) automatically.
The stack counts down not up in assembly language.
This is the current RSP point prior to loading a stack or program run:--------------------->  00007FFEC3675420
Program will now load 1-14 into registers and push to the stack after it will write the register values and current RSP pointer value again.
This will not use registers RSP/RDI they are in use for stack pointer and class subroutines.
 
RAX : 0000000000000001    R8  : 0000000000000008 
RBX : 0000000000000002    R9  : 0000000000000009 
RCX : 0000000000000003    R10 : 000000000000000A 
RDX : 0000000000000004    R11 : 000000000000000B 
RDI : 0000000000600AA5    R12 : 000000000000000C 
RSI : 0000000000000005    R13 : 000000000000000D 
RBP : 0000000000000007    R14 : 000000000000000E 
RSP : 00007FFEC36753A0    R15 : 000000000000000F

This is the current next available RSP Pointer Memory Address on top of stack:------------->  00007FFEC36753B0
Program will now load 0 into all registers and write register values after to show values are now stored in memory not registers.
 
RAX : 0000000000000000    R8  : 0000000000000000 
RBX : 0000000000000000    R9  : 0000000000000000 
RCX : 0000000000000000    R10 : 0000000000000000 
RDX : 0000000000000000    R11 : 0000000000000000 
RDI : 0000000000600B90    R12 : 0000000000000000 
RSI : 0000000000000000    R13 : 0000000000000000 
RBP : 0000000000000000    R14 : 0000000000000000 
RSP : 00007FFEC36753A0    R15 : 0000000000000000

This is the current next available RSP Pointer Memory Address on top of stack:------------->  00007FFEC36753B0
Note RSP is still using the same value. The program will now pop the stack and reverse load the values into the registers, after will write register values.
Last In First Out.
 
RAX : 000000000000000F    R8  : 0000000000000009 
RBX : 000000000000000E    R9  : 0000000000000008 
RCX : 000000000000000D    R10 : 0000000000000007 
RDX : 000000000000000C    R11 : 0000000000000005 
RDI : 0000000000600C13    R12 : 0000000000000004 
RSI : 000000000000000B    R13 : 0000000000000003 
RBP : 000000000000000A    R14 : 0000000000000002 
RSP : 00007FFEC3675410    R15 : 0000000000000001

This is the the current next available RSP Pointer Memory Address after stack is popped:--->  00007FFEC3675420

Note that the RSP is now pointing to the same memory address value as when the program started.

长话短说,您可以手动将寄存器逐个加载到堆栈中,并在需要时弹出它们以恢复它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接