【问题标题】:Get return value of Linux syscall with GCC inline assembly使用 GCC 内联汇编获取 Linux 系统调用的返回值
【发布时间】:2020-06-07 02:02:49
【问题描述】:

下面的宏可以执行lstat系统调用。

#include <stdint.h>

#define m_lstat(PATH, FS){                   \
  long         _rax  = 6;  /*sys_newlstat*/  \
  uint8_t*     _path = PATH;                 \
  struct stat* _fs   = FS;                   \
  asm volatile(                              \
    "movq %0, %%rax\n"                       \
    "movq %1, %%rdi\n"                       \
    "movq %2, %%rsi\n"                       \
    "syscall"                                \
    :                                        \
    :"m"(_rax), "m"(_path), "m"(_fs)         \
    :"rax", "rdi", "rsi"                     \
  );                                         \
}

可以像 glibc 包装器一样称呼它lstat

#include <sys/stat.h>
#include <stdio.h>

int main(){
  struct stat fs0;  m_lstat("a.out", &fs0);  printf("nbytes %d\n", fs0.st_size);
  struct stat fs1;  lstat(  "a.out", &fs1);  printf("nbytes %d\n", fs1.st_size);
}

但是,如果我想访问返回值怎么办?我认为它被写入rax,但我不知道如何从 C 代码中检索它...

例如,以下操作不起作用

#define m_lstat(PATH, FS){                   \
  long         _rax  = 6;  /*sys_newlstat*/  \
  u8*          _path = PATH;                 \
  struct stat* _fs   = FS;                   \
  int ret;                                   \
  asm volatile(                              \
    "movq %0, %%rax\n"                       \
    "movq %1, %%rdi\n"                       \
    "movq %2, %%rsi\n"                       \
    "syscall"                                \
    :"=m"(ret)                               \
    :"m"(_rax), "m"(_path), "m"(_fs)         \
    :"rax", "rdi", "rsi"                     \
  );                                         \
  printf("ret %d\n", ret);                   \
}

【问题讨论】:

  • 您的代码不完整;如何重现它?
  • godbolt 链接上的这段代码应该可以工作:godbolt.org/z/yfpbE2syscall 指令本身会破坏 RCX 和 R11。我对约束使用适当的寄存器,而不是使用mS=RSI,D=RDI)。我使用输出和输入约束,将输入约束绑定到与输出约束相同的寄存器 (RAX)。这允许通过一个变量传入值并通过另一个变量传出值。我还使用了内存破坏器,因为我正在传递数据的地址(在 RSI 和 RDI 中)并且必须确保编译器在调用内联程序集之前意识到内存中的数据。
  • @tansy 我添加了一些样板,以便您可以实际编译它。
  • 您几乎从不想在内联 asm 中使用显式 mov 指令——它充其量是浪费和低效的,而且如果您没有正确使用 clobbers,通常会损坏。相反,您想使用约束。试试asm volatile("syscall" : "=a" (ret) : "a" (_rax), "D" (_path), "S" (_fs));
  • 作为 Michael 所做的补充(看起来不错),我可能会为 _rax 和 _path 添加一个 const。它不会更改生成的代码,但您的路径可能已经是一个 const 并且(不必要地)将其转换回非常量以进行此调用会很愚蠢。 lstat 不会为您获取返回值(errno)吗?使用内联 asm 执行此操作有什么价值?

标签: linux gcc x86-64 system-calls inline-assembly


【解决方案1】:

在@PeterCordes @MichaelPetch cmets 之后更新

只需使用适当的约束

inline long m_lstat(char *_path, struct stat *_fs)
{
    long _rax = 6;

    asm volatile(
        "syscall"
        : "+a" (_rax)
        : "D" (_path), "S" (_fs) 
        : "rcx", "r11", /* used by syscall */
          "memory"      /* barrier for _path and _fs */
    );

    return _rax;
}

这段代码

struct stat s;
char foo[] = "foo";

long test()
{
    return m_lstat(foo, &s);
}

生产

test:
    movl    $6, %eax
    leaq    foo(%rip), %rdi
    leaq    s(%rip), %rsi
#APP
# 10 "m_lst.c" 1
    syscall
# 0 "" 2
#NO_APP
    ret

我使用此代码进行测试,一切正常

int main(int argc, char **argv)
{
    struct stat fs;
    long ret;
    char *p = argv[ argc >= 2 ];

    ret = lstat(p, &fs);
    printf("lstat:   %s: ret = %ld, size = %zd\n", p, ret, fs.st_size);

    ret = m_lstat(p, &fs);
    printf("m_lstat: %s: ret = %ld, size = %zd\n", p, ret, fs.st_size);

    return 0;
}

附言如果由于某些原因你想使用宏而不是内联函数,你应该使用另一种语法:

#define m_lstat(_path, _fs) \
({                          \
    long _rax = 6;          \
    asm volatile (          \
        "syscall"           \
        : "+a" (_rax)       \
        : "D" (_path), "S" (_fs) \
        : "rcx", "r11", "memory" \
    );                      \
    _rax;                   \
})

【讨论】:

  • 关闭,但请阅读有关问题的 cmets:您忘记了 RCX 和 R11 clobbers(使用 syscall 固有),并且您忘记了 "memory" clobber 或虚拟内存输出操作数来告诉编译器输入的指向的内存也分别是输入和输出。 (您可以通过使用本地数组作为输入来破坏您的版本:gcc 将优化字符串字节的“死”存储,因为您没有告诉它任何内容读取它。)另请参阅 MUSL libc syscall2 宏工作示例。
  • @étale-cohomology 我确实错了。我在更简单的情况下使用内联汇编。
  • 我在问题下的评论也提到了RCX和R11。 syscall 指令记录在这里:felixcloutier.com/x86/syscall。 RCX 被syscall 之后的指令地址破坏,R11 通过保存 RFLAGS 的状态被破坏。因为它们被破坏了,所以您需要通过将 RCX 和 R11 添加到 clobbers 列表来告诉内联程序集可能会修改它们。
  • @étale-cohomology:这个答案的危险之处在于它恰好适用于某些情况(因为"+a""D""S" 约束是正确的),但可以当您更改周围的代码和/或在启用优化的情况下编译时中断(因为缺少clobbers)。克里斯在问题下的评论也有同样的问题。 测试不足以证明 inline-asm 正确。 错误很容易犯,而且很难测试。但是当你看到它们时,构建测试用例并不难,在实践中对编译器撒谎确实会破坏。例如路径 = 本地数组。
  • 可以在此处找到大多数版本的 GCC 可以与 -O1 或更高优化级别(当未在系统调用上将 RCX 指定为 clobber 时)中断的示例:godbolt.org/z/u2h9o-。该示例有一个函数setsix,我在m_lstat 之前调用了它。这是一个带有内联汇编器的简单函数,仅用于返回值 6(但通过 RCX 寄存器完成)。然后我检查setsix 返回的值在m_lstat 返回之后是否仍然相同。如果它们不同,那是因为 RCX 被意外修改了。在 -O0 优化级别它应该可以工作。
【解决方案2】:

如果你删除printf(); 调用,你会得到这个:

.LC0:
        .string "test.txt"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 56
.LBB2: // m_lstats
        mov     QWORD PTR [rbp-8], 6 // _rax  = 6;
        mov     QWORD PTR [rbp-16], OFFSET FLAT:.LC0 // _path = PATH;
        lea     rax, [rbp-176] // _fs = FS;
        mov     QWORD PTR [rbp-24], rax
        mov     rax, QWORD PTR [rbp-8] // %rax = _rax
        mov     rdx, QWORD PTR [rbp-16] // %rdx = _path
        mov     rsi, QWORD PTR [rbp-24] // %rsi = _fs
        mov     rdi, rdx // %rdi = _path
        syscall // call
        mov     DWORD PTR [rbp-28], eax // ret = %eax (low %rax)
.LBE2: // back to main
        mov     eax, 0 // return 0; // @ main()
        leave
        ret

剩下的只是为printf();做准备(回到printf)

.LC0:
        .string "test.txt"
.LC1:
        .string "ret %d\n"
(...)
        syscall // call
        mov     DWORD PTR [rbp-28], eax // (*)ret = %eax (low %rax)
        mov     eax, DWORD PTR [rbp-28] // %esi = &ret
        mov     esi, eax //
        mov     edi, OFFSET FLAT:.LC1 // %edi = &"ret %d\n"
        mov     eax, 0 // %eax = 0 // ?
        call    printf

因此,您确实在ret (mov DWORD PTR [rbp-28], eax) 中获得了返回值,正如您所说,返回值在%rax 中。 因此,您应该能够像访问任何普通变量一样访问ret

PS。希望你不介意英特尔语法与%reg 混合,% 表示寄存器与变量相对。我知道不会有 rax 变量,但为了使其更具可读性,我使用了这个装饰。

【讨论】:

  • 谢谢!但是我在ret 中仍然一无所获……我的 GCC 内联汇编肯定有问题……也许是"=m"(ret) 部分……
  • 你绝对需要它内联吗?如果你把这个函数做成一个模块呢?您可以在汇编中执行它并从 C 中调用它。
猜你喜欢
  • 1970-01-01
  • 2018-04-13
  • 2016-01-16
  • 2015-04-30
  • 2012-02-11
  • 2021-05-01
  • 1970-01-01
  • 2011-03-27
相关资源
最近更新 更多