在64位Linux和64位处理器上运行32位汇编代码:解释异常情况

15

我遇到了一个有趣的问题。我忘记了我正在使用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版本吗?

3个回答

8
请记住,默认情况下,64位操作系统中的所有内容都假定为64位。您需要确保(a)在适当的情况下使用32位版本的#include文件(b)链接32位库和(c)构建32位可执行文件。如果您有makefile,请显示其内容,否则请显示用于构建此示例的命令。
顺便说一句,我稍微修改了您的代码(_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
@claws: 我只是为了让代码能够轻松地与gcc一起构建和链接而进行了那个更改,但我想这也意味着在调用main函数之前会运行C运行时库启动代码。 - Paul R
我已经编辑了我的问题。另外,当我尝试使用gcc -Wall test.S -m32 -o test编译您的代码时,它会出现以下错误(下划线用作分隔符):/usr/bin/ld: 在搜索-lgcc时跳过不兼容的/usr/lib/gcc/x86_64-linux-gnu/4.4.1/libgcc.a
/usr/bin/ld: 在搜索-lgcc时跳过不兼容的/usr/lib/gcc/x86_64-linux-gnu/4.4.1/libgcc.a
/usr/bin/ld: 找不到-lgcc
collect2: ld 返回 1 的退出状态
- claws
2
@claws:看起来你的系统上没有安装32位gcc或库。你需要安装适用于你的系统的32位开发包以使用-m32。 - Chris Dodd
1
@claws:解决汇编语言编写中的相关问题的一个有用技巧是,首先使用简单的C示例,然后使用gcc -S进行编译以生成汇编源代码-这样可以通过实际工作代码展示如何处理ABI、系统调用等。比从文档和第一原理等方面解决所有问题要容易得多。 - Paul R

6
如Paul所指出的,如果您想在64位系统上构建32位二进制文件,则需要使用-m32标志。由于某些64位Linux发行版默认不包括32位编译器/链接器/库支持,因此该标志可能在您的安装中不可用。
另一方面,您可以将代码构建为64位,此时需要使用64位调用约定。在这种情况下,系统调用号放在%rax中,而参数放在%rdi、%rsi和%rdx中。 编辑 我发现最好的地方是www.x86-64.org,具体来说是abi.pdf。

谢谢提到64位约定。我正在拼命寻找它。我想了解更多关于64位约定的信息。你能给我一些链接吗?(最好是官方链接)。 - claws

1
64位的CPU可以运行32位的代码,但必须使用特殊模式来实现。这些指令在64位模式下都是有效的,因此您可以构建一个64位可执行文件。使用“gcc -m32 -nostdlib hello.S”编译和运行您的代码是正确的。这是因为“-m32”定义了“__i386”,所以“/usr/include/asm/unistd.h”包括“”,其中包含适用于“int $0x80” ABI的正确常量。有关libc和静态与动态可执行文件的_start vs. main的更多信息,请参见在64位系统上汇编32位二进制文件(GNU工具链)
$ 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位模式下工作:如果在64位代码中使用32位int 0x80 Linux ABI会发生什么?但是,在64位代码中不推荐使用int 0x80。(实际上,从来没有推荐过)为了提高效率,32位代码应通过内核导出的VDSO页面调用,以便在支持它的CPU上使用sysenter进行快速系统调用。


“但这并没有回答我的问题。在这种情况下到底发生了什么?”
“好问题。”
“在Linux上,无论调用进程处于什么模式下,使用eax=1的int $0x80都是sys_exit(ebx)。虽然32位ABI在64位模式下可用(除非你的内核没有编译i386 ABI支持),但不要使用它。你的退出状态来自movl $(STDOUT),%ebx。(顺便说一句,在unistd.h中定义了一个STDOUT_FILENO宏,但由于它还包含无效的asm语法的C原型,因此无法从.S文件中包含它)。”
请注意,unistd_32.h中的__NR_exitunistd_64.h中的__NR_write都是1,因此您的第一个int $0x80退出了进程。您正在调用错误的ABI系统调用号码。

strace以64位进程预期使用的ABI调用约定解码错误,就像您已经调用了syscall一样。UNIX和Linux x86-64系统调用的调用约定是什么

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

_start入口时rdirsi0(又名NULL),您的代码使用movl $(helloend-hellostr) , %edxrdx设置为12

Linux在execve之后会将新进程中的寄存器初始化为零。(ABI规定为未定义,Linux选择零以避免信息泄漏)。在静态链接可执行文件中,_start是第一个运行的用户空间代码。(在动态可执行文件中,动态链接器在_start之前运行,并且会在寄存器中留下垃圾)。
另请参见标签wiki,了解更多汇编链接。

很好的回答,您能解释一下在excve之后不将寄存器设置为0的影响吗?那会有什么危害? - Trey
1
@Trey:内核不想将任何内核数据泄露给不受信任的用户空间进程。Linux是一个多用户操作系统,它会注意隔离用户之间的关系,而且无法预测某些敏感信息(例如密码,或更有可能是对攻击者尝试利用内核漏洞的内存地址)是否会留在寄存器中。 - Peter Cordes
如果在64位机器上,使用rax寄存器等于60的int $0x80会发生什么?我刚试了一下,它会产生一个SIGSEV,为什么会这样? - Trey
1
@Trey:因为那不是 sys_exit,所以你掉进了一个导致段错误的东西。在调试器中单步执行它。更重要的是,阅读 https://dev59.com/fFYO5IYBdhLWcg3wRfd-,在第一段中就有你问题的答案。这个答案也解释了为什么。 - Peter Cordes

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