【问题标题】:"Segmentation fault", x86_64 assembly, AT&T syntax“分段错误”、x86_64 程序集、AT&T 语法
【发布时间】:2016-08-07 03:26:43
【问题描述】:

我在 64 位 Linux 环境中运行我的代码,其中 Linux 内核是使用 IA32_EMULATIONX86_X32 禁用构建的。 p>

在书Programming from the Ground Up 中,第一个程序除了产生段错误之外什么都不做:

.section .data

.section .text
.globl _start
_start:
movl $1, %eax
movl $0, %ebx

int $0x80

我将代码转换为使用 x86-64 指令,但它也有段错误:

.section .data

.section .text
.globl _start
_start:
movq $1, %rax
movq $0, %rbx

int $0x80

我是这样组装这两个程序的:

as exit.s -o exit.o
ld exit.o -o exit

运行./exit 为两者提供Segmentation fault。我做错了什么?

附:我看过很多使用 gcc 组装代码的教程,但是我想使用 gas

更新

结合cmets和答案,这是代码的最终版本:

.section .data
.section .text

.globl _start
_start:

movq $60, %rax
xor  %rbx, %rbx

syscall

【问题讨论】:

  • int 0x80 是否仍然存在于 x86_64 中?我以为它已经被syscall指令取代了
  • @Pierre 将 int 0x80 替换为 syscall 也给了我 Segmentation fault
  • @MichaelPetch 我使用自定义内核并禁用 i386 仿真,因为我从不运行 32 位二进制文​​件。
  • 这就是你出现段错误的原因。大多数发行版都打开 IA32 仿真。如果您将其关闭,您将无法在 64 位代码中使用 int 0x80。您需要使用 64 位 syscall 指令和约定。 blog.rchapman.org/post/36801038863/… 是一个很好的起点。如果您打开 IA32 仿真,您的内核可以在 64 位代码中使用int 0x80,但仅限于 32 位操作数和 32 位指针。 IA32 仿真不使用高 32 位。一个缺点是基于堆栈的指针不能传递给int 0x80
  • 在编写 x86-64 代码时首选syscall 指令。调用约定和编号不同于int 0x80

标签: linux assembly x86-64 system-calls gnu-assembler


【解决方案1】:

int $0x80 是 32 位 ABI。在普通内核(使用 IA32 仿真编译)上,它在 64 位进程中可用,但您不应该使用它,因为它仅支持 32 位指针,并且某些结构具有不同的布局。

有关进行 64 位 Linux 系统调用的信息,请参阅 标签 wiki。 (也是ZX485对这个问题的回答)。有很多不同之处,包括 syscall 指令破坏了 %rcx%r11,这与 int $0x80 ABI 不同。

在没有 IA32 仿真的内核中,比如你的内核,运行 int $0x80 可能与运行任何其他无效软件中断相同,比如 int $0x79。在 gdb 中单步执行该指令(在我的包含 IA32 仿真的 64 位 4.2 内核上)会导致该指令出现段错误。

它不会返回并继续执行垃圾字节作为指令(这也会导致 SIGSEGV 或 SIGILL),或者继续执行直到它跳转到(或正常到达)未映射的页面。如果是这样,那将是段错误的机制。

您可以在strace 下运行进程,例如strace /bin/true --version 以确保它正在进行您认为会的系统调用。您还可以使用gdb 查看程序段错误的位置。 使用调试器是必不可少的,比大多数语言更重要,因为 asm 中的故障模式通常只是一个段错误。

【讨论】:

【解决方案2】:

第一个观察结果是,您的两个示例中的代码都有效地执行了相同的操作,但编码不同。站点x86-64.org 为那些开始进行x86-64 开发的人提供了一些很好的信息。第一个使用 32 位寄存器的代码 sn-p 等价于第二个,因为Implicit Zero Extend

隐式零扩展

32 位运算的结果隐式零扩展为 64 位值。这与 16 位和 8 位操作不同,后者不会影响寄存器的上部。这在某些情况下可用于代码大小优化,例如:

movl $1, %eax                 # one byte shorter movq $1, %rax
xorq %rax, %rax       # three byte equivalent of mov $0,%rax
andl $5, %eax         # equivalent for andq $5, %eax

问题是,为什么这段代码会出现段错误?如果您在典型的 x86-64 Linux 发行版上运行此代码,您的代码可能会按预期退出,而不会生成 segfault。您的代码失败的原因是因为您使用的是禁用 IA32 仿真的自定义内核。

Linux 内核中的 IA32 仿真允许您使用 32 位 int 0x80 中断来使用传统的 32-bit system call mechanism 进行调用。这是一个仿真层,不支持传递无法在 32 位寄存器中表示的指针。基于堆栈的指针就是这种情况,因为它们位于 4gb 地址空间之外,并且不能用 32 位指针访问。

您的系统关闭了 IA32 仿真,因此不存在 int 0x80 以实现向后兼容性。结果是int 0x80 中断将引发分段错误,您的应用程序将失败。

在 x86-64 代码中,最好使用 syscall 指令对 64 位 Linux 内核进行系统调用。此机制在必要时支持 64 位操作数和指针。 Ryan Chapman 的网站上有一些关于 64-bit SYSCALL 接口的好信息,这与 32 位 int 0x80 机制有很大不同。

如果没有 IA32 仿真,您的代码可以在 64 位环境中以这种方式编写:

.section .text

.globl _start
_start:

mov  $60, %eax
xor  %ebx, %ebx
syscall

可以在64-bit System V ABI 中找到有关进行 64 位开发的其他有用信息。本文档还更好地描述了 Linux 内核使用的一般 syscall 约定,包括A.2 节中的副作用。如果您还希望与第三方库和模块(如 C 库等)进行交互,此文档也非常有用。

【讨论】:

  • 这很有趣,x86-64.org 上的示例错误计算了字节节省。 gas 组合 48 c7 c0 01 00 00 00 mov $0x1,%rax (7B),使用 REX mov r/m64, imm32(sign-extended) 形式。和b8 01 00 00 00 mov $0x1,%eax (5B),使用mov r32, imm32 形式将dest reg 编码到操作码中,而不是mod/rm 字节。 (它的 REX 形式是movabs r64, imm64,这就是为什么mov r64, imm32 必须使用r/m64 编码。)不过答案很好。我的只是解释了它出现故障的确切原因,而不是该怎么做。
【解决方案3】:

原因是Linux System Call Table for x86_64 与 x86 的表不同。

在 x86 中,SYS_EXIT 是 1。在 x64 中,SYS_EXIT 是 601 是 SYS_WRITE 的值,如果调用它,则需要 %RSI 中的 const char *buf。如果该缓冲区指针无效,则可能是段错误。

%rax    System call   %rdi              %rsi            %rdx      %r10  %r8 %r9
1       sys_write     unsigned int fd   const char *buf size_t 
60      sys_exit      int error_code    

【讨论】:

  • 这是真的。但是,如果内核是使用 IA32 仿真构建的(大多数情况下,除非您构建一个自定义的将其关闭),则 int 0x80 将被仿真。 64 位代码中的 int 0x80 仅限于使用 32 位操作数(显然),但仍符合 IA32 int 0x80 约定。我在 cmets 中查询了 OP,他确认他关闭了 IA32 仿真。如果他打开它,这个代码就会起作用。当然syscall 是 64 位代码中的首选方法,因为您可以传递 64 位指针和其他 64 位操作数。在他的情况下,segault 是因为int 0x80 没有被他的内核模拟。
  • 感谢您的指出。结果代码是movq $60, %rax 而不是movq $1, %raxsyscall 而不是int $0x80
  • 请注意,write(2) 如果您传递一个错误的指针,则返回 -EFAULT。 @Cág:这就是为什么你应该使用strace 来查看你的程序做了什么。可能您的系统调用返回,然后在int $0x80 之外继续执行,并且那里的数据在解码为 x86 指令时会导致段错误。或者继续执行,直到指令获取本身导致段错误。这个答案很有用,但根本不能解释观察到的行为,因为 32 位 int $0x80 ABI 仍然可以从 64 位进程访问,正如 Michael 指出的那样。
  • @PeterCordes :你错过了一些东西。在这种情况下,OP 代码不起作用,因为他的内核是作为自定义内核构建的,并且关闭了 IA32 仿真。他的代码失败了,因为他的环境根本不支持int 0x80。我第一次回答他的问题是问他是否碰巧在关闭 IA32 仿真的情况下构建了一个自定义内核,就是这样。这个问题是另一个问题,我只是找不到。最近有人在关闭 IA32 的情况下运行了自定义内核并遇到了同样的问题。他别无选择,只能使用syscall 或使用 IA32 仿真重建他的内核。
  • @Cág :您的问题中显示的代码不使用 syscall 指令,它使用 int 0x80 这是一种不同的机制。我有一部分人认为,如果您按原样使用您在此处发布的第一个示例代码并构建它(并运行它),并且您关闭了 IA32 仿真,它应该会出现段错误(不能正常退出)。您在问题中发布的 2 个示例在关闭 IA32 仿真的环境中都应该失败(我很确定)。
猜你喜欢
  • 2021-03-05
  • 1970-01-01
  • 2019-04-04
  • 2013-06-30
  • 1970-01-01
  • 2021-01-23
  • 1970-01-01
相关资源
最近更新 更多