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. 解释一下write(0, NULL, 12)strace输出中系统调用的参数?
  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 版本吗?

4

3 回答 3

8

请记住,默认情况下,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
于 2010-03-23T15:03:59.810 回答
6

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

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

编辑

我找到的最好的地方是www.x86-64.org,特别是abi.pdf

于 2010-03-24T04:47:34.527 回答
1

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

您的代码使用gcc -m32 -nostdlib hello.S. 那是因为-m32定义了__i386,所以 /usr/include/asm/unistd.h包含了,它具有ABI<asm/unistd_32.h>的正确常量。int $0x80

另请参阅Assembling 32-bit binaries on a 64-bit system (GNU toolchain)了解更多关于_startvs. mainwith/without libc 和静态 vs.动态可执行文件的信息。

$ 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 会发生什么?int 0x80不建议在 64 位代码中使用。(实际上,从不推荐它。为了提高效率,32 位代码应该通过内核导出的 VDSO 页面调用,以便它可以sysenter用于支持它的 CPU 上的快速系统调用)。


但这并不能回答我的问题。在这种情况下到底发生了什么?

好问题。

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

(顺便说一句,在STDOUT_FILENO中定义了一个宏unistd.h,但不能#include <unistd.h>从 a中定义,.S因为它还包含不是有效 asm 语法的 C 原型。)

请注意,__NR_exitfromunistd_32.h__NR_writefromunistd_64.h都是1,所以您首先 int $0x80退出您的流程。您为正在调用的 ABI 使用了错误的系统调用号。


strace解码不正确,就像您调用了它一样syscall(因为这是 ABI,预计将使用 64 位进程)。 x86-64 上 UNIX 和 Linux 系统调用的调用约定是什么

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

rdi并且rsi0(又名NULL)进入_start,并且您的代码集rdx=12movl $(helloend-hellostr) , %edx.

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

另请参阅标签 wiki 以获取更多 asm 链接。

于 2017-09-03T04:06:19.303 回答