【问题标题】:Running 32 bit assembly code on a 64 bit Linux & 64 bit Processor : Explain the anomaly在 64 位 Linux 和 64 位处理器上运行 32 位汇编代码:解释异常
【发布时间】:2011-01-30 19:37:34
【问题描述】:

我遇到了一个有趣的问题。我忘记了我使用的是 64 位机器和操作系统并编写了一个 32 位汇编代码。我不知道如何编写 64 位代码。

这是 Linux 上 Gnu Assembler(AT&T 语法)的 x86 32 位汇编代码。

//hello.S
#include <asm/unistd.h>
#include <syscall.h>
#define STDOUT 1

.data
hellostr:
    .ascii "hello wolrd\n";
helloend:

.text
.globl _start

_start:
    movl $(SYS_write) , %eax  //ssize_t write(int fd, const void *buf, size_t count);
    movl $(STDOUT) , %ebx
    movl $hellostr , %ecx
    movl $(helloend-hellostr) , %edx
    int $0x80

    movl $(SYS_exit), %eax //void _exit(int status);
    xorl %ebx, %ebx
    int $0x80

    ret

现在,这段代码应该可以在 32 位处理器和 32 位操作系统上正常运行,对吧?众所周知,64 位处理器向后兼容 32 位处理器。所以,这也不是问题。问题的出现是因为 64 位操作系统和 32 位操作系统中的系统调用和调用机制不同。我不知道为什么,但他们在 32 位 linux 和 64 位 linux 之间更改了系统调用号。

asm/unistd_32.h 定义:

#define __NR_write        4
#define __NR_exit         1

asm/unistd_64.h 定义:

#define __NR_write              1
#define __NR_exit               60

无论如何,使用宏而不是直接数字是有回报的。它确保正确的系统调用号。

当我组装&链接&运行程序时。

$cpp hello.S hello.s //pre-processor
$as hello.s -o hello.o //assemble
$ld hello.o // linker : converting relocatable to executable

它不打印helloworld

在 gdb 中显示:

  • 程序以代码 01 退出。

我不知道如何在 gdb 中调试。使用教程我尝试调试它并在每一步通过指令检查寄存器执行指令。它总是向我显示“程序以 01 退出”。如果有人能告诉我如何调试,那就太好了。

(gdb) break _start
Note: breakpoint -10 also set at pc 0x4000b0.
Breakpoint 8 at 0x4000b0
(gdb) start
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Temporary breakpoint 9 (main) pending.
Starting program: /home/claws/helloworld 

Program exited with code 01.
(gdb) info breakpoints 
Num     Type           Disp Enb Address            What
8       breakpoint     keep y   0x00000000004000b0 <_start>
9       breakpoint     del  y   <PENDING>          main

我尝试运行strace。这是它的输出:

execve("./helloworld", ["./helloworld"], [/* 39 vars */]) = 0
write(0, NULL, 12 <unfinished ... exit status 1>
  1. 解释一下strace输出中write(0, NULL, 12)系统调用的参数?
  2. 究竟发生了什么?我想知道确切地它以exitstatus=1 退出的原因?
  3. 谁能告诉我如何使用 gdb 调试这个程序?
  4. 他们为什么要更改系统调用号?
  5. 请适当更改此程序,以便它可以在这台机器上正常运行。

编辑:

阅读 Paul R 的回答后。我检查了我的文件

claws@claws-desktop:~$ file ./hello.o 
./hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

claws@claws-desktop:~$ file ./hello
./hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

我同意他的观点,这些应该是 ELF 32 位可重定位和可执行的。但这并不能回答我的问题。我所有的问题仍然是问题。在这种情况下到底发生了什么?有人可以回答我的问题并提供此代码的 x86-64 版本吗?

【问题讨论】:

    标签: linux assembly gdb x86 x86-64


    【解决方案1】:

    请记住,默认情况下,64 位操作系统上的所有内容都倾向于假定为 64 位。您需要确保 (a) 在适当的情况下使用 32 位版本的 #includes (b) 链接 32 位库和 (c) 构建 32 位可执行文件。如果你有你的makefile的内容,或者你用来构建这个例子的命令,这可能会有所帮助。

    FWIW 我稍微更改了您的代码(_start -> main):

    #include <asm/unistd.h>
    #include <syscall.h>
    #define STDOUT 1
    
        .data
    hellostr:
        .ascii "hello wolrd\n" ;
    helloend:
    
        .text
        .globl main
    
    main:
        movl $(SYS_write) , %eax  //ssize_t write(int fd, const void *buf, size_t count);
        movl $(STDOUT) , %ebx
        movl $hellostr , %ecx
        movl $(helloend-hellostr) , %edx
        int $0x80
    
        movl $(SYS_exit), %eax //void _exit(int status);
        xorl %ebx, %ebx
        int $0x80
    
        ret
    

    并像这样构建它:

    $ gcc -Wall test.S -m32 -o test
    

    确认我们有一个 32 位的可执行文件:

    $ file test
    test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.6.4, dynamically linked (uses shared libs), not stripped
    

    它似乎运行正常:

    $ ./test
    hello wolrd
    

    【讨论】:

    • _startmain 会有什么不同?
    • @claws:我只是为了让代码可以轻松地与 gcc 构建和链接而进行更改,但我想这也意味着 C 运行时库启动代码将在调用 main 之前运行。跨度>
    • 我已经编辑了我的问题。此外,当我尝试使用 gcc -Wall test.S -m32 -o test 构建您的代码时,它会出现此错误(下划线用作分隔符):/usr/bin/ld: skipping incompatible /usr/lib/gcc/x86_64-linux-gnu/4.4.1 /libgcc.a 搜索 -lgcc ______________ /usr/bin/ld: 搜索 -lgcc 时跳过不兼容的 /usr/lib/gcc/x86_64-linux-gnu/4.4.1/libgcc.a ______________ /usr/bin/ ld: 找不到 -lgcc ______________ collect2: ld 返回 1 个退出状态
    • @claws:您的系统上似乎没有安装 32 位 gcc 或库。您需要为您的系统安装 32 位开发包才能使用 -m32
    • @claws:对于这个和其他与编写程序集相关的问题,一个有用的技术是首先使用一个简单的 C 示例,然后使用 gcc -S 编译它以生成程序集源 - 然后向您展示如何通过实际工作代码处理 ABI、系统调用等。比从文档和第一原则等中解决所有问题要容易得多。
    【解决方案2】:

    正如 Paul 所说,如果您想在 64 位系统上构建 32 位二进制文​​件,您需要使用 -m32 标志,默认情况下您的安装可能不提供该标志(某些 64 位 Linux 发行版默认情况下不包括 32 位编译器/链接器/lib 支持)。

    另一方面,您可以将代码构建为 64 位,在这种情况下,您需要使用 64 位调用约定。在这种情况下,系统调用号进入 %rax,参数进入 %rdi、%rsi 和 %rdx

    编辑

    我为此找到的最佳地点是www.x86-64.org,特别是abi.pdf

    【讨论】:

    • 感谢您提及 64 位约定。我正在拼命寻找那个。我想了解更多关于 64 位约定的信息。你能给我一些链接吗? (官方会更好)。
    【解决方案3】:

    64 位 CPU 可以运行 32 位代码,但它们必须使用特殊模式才能做到这一点。这些指令在 64 位模式下都有效,因此没有什么能阻止您构建 64 位可执行文件。

    您的代码可以使用gcc -m32 -nostdlib hello.S 正确构建和运行。这是因为-m32 定义了__i386,所以/usr/include/asm/unistd.h 包含&lt;asm/unistd_32.h&gt;,它具有int $0x80 ABI 的正确常量。

    另请参阅 Assembling 32-bit binaries on a 64-bit system (GNU toolchain) 了解更多关于 _startmain 有/无 libc 以及静态与动态可执行文件的信息。

    $ file a.out 
    a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, BuildID[sha1]=973fd6a0b7fa15b2d95420c7a96e454641c31b24, not stripped
    
    $ strace ./a.out  > /dev/null
    execve("./a.out", ["./a.out"], 0x7ffd43582110 /* 64 vars */) = 0
    strace: [ Process PID=2773 runs in 32 bit mode. ]
    write(1, "hello wolrd\n", 12)           = 12
    exit(0)                                 = ?
    +++ exited with 0 +++
    

    从技术上讲,如果您使用了正确的索书号,您的代码也可以在 64 位模式下工作:What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? 但不建议在 64 位代码中使用 int 0x80。 (实际上,不建议这样做。为了提高效率,32 位代码应该通过内核导出的 VDSO 页面调用,以便它可以使用sysenter 在支持它的 CPU 上进行快速系统调用)。


    但这并不能回答我的问题。在这种情况下,究竟发生了什么

    好问题。

    在 Linux 上,int $0x80eax=1sys_exit(ebx),无论调用进程处于何种模式。 32 位 ABI 在 64 位模式下可用(除非您的内核是在没有 i386 ABI 支持的情况下编译的),但不要使用它。您的退出状态来自movl $(STDOUT), %ebx

    (顺便说一句,在unistd.h 中定义了一个STDOUT_FILENO 宏,但您不能从.S 中定义#include &lt;unistd.h&gt;,因为它还包含无效的asm 语法的C 原型。)

    请注意,来自unistd_32.h__NR_exit 和来自unistd_64.h__NR_write 都是1,所以你的第一个 int $0x80 退出你的进程。您为正在调用的 ABI 使用了错误的系统调用号。


    strace 解码不正确,就好像您调用了 syscall(因为这是 64 位进程预期使用的 ABI)。 What are the calling conventions for UNIX & Linux system calls on x86-64

    eax=1 / syscall 表示write(rd=edi, buf=rsi, len=rdx),这就是strace 错误解码您的int $0x80 的方式。

    rdirsi 在进入_start 时是0(又名NULL),而您的代码集rdx=12movl $(helloend-hellostr) , %edx

    Linux 在 execve 之后的新进程中将寄存器初始化为零。 (ABI 说未定义,Linux 选择零以避免信息泄漏)。在静态链接的可执行文件中,_start 是第一个运行的用户空间代码。 (在动态可执行文件中,动态链接器在_start 之前运行,并且确实在寄存器中留下了垃圾)。

    有关更多 asm 链接,另请参阅 标签 wiki。

    【讨论】:

    • 可靠的答案,你能解释一下在 excve 之后不将寄存器设置为 0 的含义吗?这怎么可能有害?
    • @Trey:内核不想将任何内核数据泄露给不受信任的用户空间进程。 Linux 是一个多用户操作系统,它注意将用户彼此隔离,并且无法预测某些敏感信息(如密码,或者更可能是对试图利用内核漏洞的攻击者有用的内存地址)是否可能碰巧被留在登记册里。
    • 如果在 64 位机器上使用 int $0x80rax 寄存器等于 60 会怎样?我刚试过,它给了一个SIGSEV,这是为什么呢?
    • @Trey:因为那不是sys_exit,所以你陷入了段错误。在调试器中单步执行。更重要的是,请阅读stackoverflow.com/questions/46087730/…,您的问题的答案在第一段中。这个答案也解释了原因。
    猜你喜欢
    • 1970-01-01
    • 2015-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-17
    • 2016-01-02
    • 2013-02-23
    • 2013-04-08
    相关资源
    最近更新 更多