为什么x86-64 Linux系统调用会修改RCX寄存器的值,这个值代表什么意思?

15

我正在尝试使用Linux的sys_brk系统调用来分配一些内存。这是我尝试过的:

BYTES_TO_ALLOCATE equ 0x08

section .text
    global _start

_start:
    mov rax, 12
    mov rdi, BYTES_TO_ALLOCATE
    syscall

    mov rax, 60
    syscall
根据 Linux 调用规范,我预期返回值将在 rax 寄存器中(指向已分配内存的指针)。我在 gdb 中运行了这个程序,在进行 sys_brk 系统调用后,发现以下寄存器内容。
系统调用前:
rax            0xc      12
rbx            0x0      0
rcx            0x0      0
rdx            0x0      0
rsi            0x0      0
rdi            0x8      8

系统调用后

rax            0x401000 4198400
rbx            0x0      0
rcx            0x40008c 4194444 ; <---- What does this value mean?
rdx            0x0      0
rsi            0x0      0
rdi            0x8      8

在这种情况下,我不太理解rcx寄存器中的价值。哪一个作为指向我用sys_brk分配的8字节开头的指针使用?


6
S YSCALL 指令会影响到 RCX 和 R11 的值(在将 SYSCALL 指令之后的指令地址保存到 RCX 寄存器后)。同时,RFLAGS 寄存器的值会被存储到 R11 寄存器中。 - Michael Petch
非常有趣。这意味着为了在之后使用 cl 寄存器,我需要先将其清除,对吗?我的意思是例如 xor cl, cl 然后 mov cl, 7 - St.Antario
在 SYSCALL 之后,您不能依赖 RCXR11 的值。因此,您必须使用其他寄存器之一代替 RCXR11_(以及 RAX),或者您将不得不保存该值(例如堆栈)并在之后恢复它。_RCXR11 不是由您设置的,您只是不能使用它们并期望在 SYSCALL 之前和之后它们保持相同。 - Michael Petch
@MichaelPetch 但是仅仅清空有什么问题呢? - St.Antario
2
在 SYSCALL 调用之前清除它将被 SYSCALL 覆盖。SYSCALL 将只覆盖其中的内容。如果您希望,在 SYSCALL 之后可以设置它,但是如果您执行另一个 SYSCALL,则该值将被破坏。 - Michael Petch
1个回答

20
系统调用的返回值总是在rax中。请参阅UNIX和Linux系统调用的调用约定在i386和x86-64上是什么
请注意,sys_brk的接口与brk/sbrk POSIX函数略有不同,请参见Linux brk(2)手册页的C库/内核差异部分。具体来说,Linux sys_brk 设置程序断点;参数和返回值都是指针。请参阅Assembly x86 brk() call use。那个答案需要得到赞,因为它是该问题唯一好的答案。
你的问题中另一个有趣的部分是:

我不太理解在这种情况下rcx寄存器的价值

您正在看到syscall / sysret指令设计的机制,以允许内核恢复用户空间执行但仍然快速。 syscall不执行任何加载或存储操作,它只修改寄存器。 它没有使用特殊寄存器保存返回地址,而是使用常规整数寄存器。
在内核返回到用户空间代码后,RCX=RIPR11=RFLAGS并非巧合。唯一的例外是当ptrace系统调用在进程位于内核内部时修改了保存的rcxr11值。在这种情况下,Linux将使用iret而不是sysret返回到用户空间,因为较慢的通用iret可以做到这一点。(有关Linux系统调用入口点的一些演练,请参见如果您在64位代码中使用32位int 0x80 Linux ABI会发生什么?。虽然大多数入口点来自32位进程,而不是来自64位进程中的syscall。)

syscall不像int 0x80一样将返回地址压入内核栈,而是:

  • 将RCX=RIP,R11=RFLAGS (所以内核甚至无法看到在执行syscall之前这些寄存器的原始值)。

  • 使用配置寄存器中预配置的掩码屏蔽RFLAGS (IA32_FMASK MSR)。这使得内核可以禁用中断(IF),直到完成swapgs并将rsp设置为指向内核堆栈。即使在入口点处使用cli作为第一条指令,也会存在漏洞窗口。通过屏蔽DF,你还可以免费获得cld,因此rep movs/stos即使用户空间使用了std,它们也会向上移动。

    有趣的事实:AMD最初提出的syscall / swapgs设计没有屏蔽RFLAGS,但他们在amd64邮件列表上得到内核开发人员的反馈后进行了更改(大约在2000年,在第一块硅片出现几年前)。

  • 跳转到已配置的syscall入口点(设置CS:RIP = IA32_LSTAR)。旧的CS值没有保存在任何地方,我想。

  • 它不执行任何其他操作,内核必须使用swapgs来访问信息块,其中保存了内核堆栈指针,因为rsp仍然具有其来自用户空间的值。

因此,syscall的设计需要一个会破坏寄存器的系统调用ABI,这就是为什么这些值是它们现在的样子。


“sysret”指令的用例是什么?您提供的链接提到它是“syscall”的伴随指令。但我从未见过在“syscall”指令之后使用“sysret”! - Sourav Kannantha B
@SouravKannanthaB:syscall 调用进入内核,内核中的 sysret 返回到用户空间。因此,原因与为什么在 call printf 后不使用 ret 相同,除非那是函数的结尾。请参阅 What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? 了解有关内核 int 0x80 和 syscall 入口点如何工作的详细信息。 - Peter Cordes
如果保存的 RCX != RIPR11 != RFLAGS,Linux内核将使用iret而不是sysret。为什么不仅恢复 %rcx/%r11 与保存的 RIP/RFLAGS 并使用 sysret (我认为这会更快?) - Fang Zhen
@FangZhen:由于CPU / ISA设计缺陷。例如,如果RIP是非规范的,则CPU将#GP。但是,英特尔CPU将处理该异常而不更新RSP(因此它是用户堆栈),但CPU仍处于内核模式。用户空间可以通过使用ptrace创建非规范RIP并使另一个线程修改正在用作内核堆栈的内存来轻松利用它。因此需要进行一些检查,并且由于ptrace或信号更改其他任务的寄存器很少见,因此最快的方法是使用简单的检查。 - Peter Cordes
@FangZhen:请参见 https://github.com/torvalds/linux/blob/e7d0c41ecc2e372a81741a30894f556afec24315/arch/x86/entry/entry_64.S#L260 作为示例。 - Peter Cordes
显示剩余2条评论

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