NASM x86_64汇编在32位模式下:为什么这个指令会产生RIP相对寻址代码?

3
[bits 32]
    global _start

    section .data
    str_hello       db  "HelloWorld", 0xa
    str_hello_length    db      $-str_hello

    section .text

    _start:

        mov ebx, 1              ; stdout file descriptor
        mov ecx, str_hello      ; pointer to string of characters that will be displayed        
        mov edx, [str_hello_length] ; count outputs Relative addressing
        mov eax, 4              ; sys_write
        int 0x80                ; linux kernel system call

        mov ebx, 0  ; exit status zero
        mov eax, 1  ; sys_exit
        int 0x80    ; linux kernel system call

这里的关键是我需要获得hello字符串的长度以便作为参数传递给Linux的sys_write系统调用。现在,我知道可以使用EQU就可以顺利工作,但我真的想了解这里发生了什么。
基本上,当我使用EQU时,它会加载值,这很好。
str_hello_length equ $-str_hello
...
...
mov edx, str_hello_length

然而,如果我在使用DB时使用此行代码。
str_hello_length db $-str_hello
...
...
mov edx, [str_hello_length]     ; of course, without the brackets it'll load the address, which I don't want. I want the value stored at that address

与我预期的加载该地址处的值不同,汇编器输出了RIP相对寻址(在gdb调试器中显示),我只是想知道为什么。

mov    0x6000e5(%rip),%edx        # 0xa001a5

现在,我尝试使用eax寄存器(然后将eax移动到edx),但是我遇到了不同的问题。在gdb中注意到,我最终会得到一个分段错误:

movabs 0x4b8c289006000e5,%eax

看起来不同的寄存器会产生不同的代码。我猜我需要以某种方式截断高32位,但我不知道怎么做。

虽然我找到了一个“解决方案”,方法如下: 使用 str_hello_length 的地址加载 eax,然后加载 eax 指向的地址的内容,一切都很顺利。

mov eax, str_hello_length       
mov edx, [eax]  ; count


; gdb disassembly
mov    $0x6000e5,%eax
mov    (%rax),%edx

显然,试图间接从内存地址加载值会产生不同的代码?我不是很了解。

我只需要帮助理解这些指令的语法和操作,以便更好地理解如何加载有效地址。是的,我想我本可以切换到EQU并继续前进,但我真的觉得在未理解DB声明及其地址加载方式之前我无法继续。


这不是32位模式。在32位模式下,寄存器riprax不存在。因此,问题在于您认为您正在生成32位代码,而实际上您有64位代码。也就是说,在x86_64中,即使您没有在方括号中明确指定rip,生成rip相对内存地址仍然是默认设置。 - Gunther Piez
可能是我的汇编和链接方式出问题了。因为我使用的指令是:nasm -g -f elf64 $< 进行汇编,ld -o $(PROJECT_NAME) $(OBJ) 进行链接。如果我把格式改成elf32,则会出现错误提示: ld: i386 architecture of input file `main.o' is incompatible with i386:x86-64 output. - Mathmagician
使用 gcc -m32 进行链接,或者使用适当的 ld 标志 (-m elf_i386)。 - mirabilos
3个回答

8
答案是不行的。在32位模拟模式下,x86-64没有RIP相对寻址(这很明显因为在32位中RIP不存在)。发生的情况是,nasm正在编译一些可爱的32位操作码,而您正在尝试将其作为64位运行。GDB将您的32位操作码反汇编为64位,并告诉您在64位中,这些字节意味着一个RIP相对mov。 x86-64上的64位和32位操作码有很多重叠,以利用硅片中的公共解码逻辑,您会感到困惑,因为GDB正在反汇编的代码看起来类似于您编写的32位代码,但实际上您只是向处理器传送垃圾字节。
这与nasm无关。您正在使用错误的架构进行处理。要么在32位进程中使用32位nasm,要么针对[BITS 64]编译您的汇编代码。

1
哇!这正是问题所在!将 [bits 32] 更改为 [bit 64] 并执行 "mov edx,[str_hello_length]",它可以很好地从地址加载!看来我对32位和64位程序的理解还有欠缺。 - Mathmagician
1
这并不明显,因为RIP可能不存在,但EIP存在。 - mid
2
尽管EIP在32位模式下存在,但它不像RIP在RIP相对寻址中那样直接可访问。在32位模式下实现PIE的最直接方法是在执行“call”后从堆栈中获取返回地址。 - Ruslan

3
您正在要求汇编器以32位模式为目标(使用bits 32),但是您将该32位机器码放入64位对象文件中,然后查看在将其作为x86-64机器码反汇编时发生的情况。因此,您会看到x86-32和x86-64指令编码之间的差异。也就是说,这就是将32位机器码解码为64位时所发生的情况。

mov 0x6000e5(%rip),%edx # 0xa001a5

在这种情况下,32位x86有两种冗余的方式来编码32位绝对地址(没有寄存器):带或不带SIB字节。32位模式没有RIP相对(或EIP相对)寻址。

x86-64将较短的(ModR/M + disp32)形式重新用作RIP相对寻址模式,而32位绝对寻址仍然可用较长的ModR/M + SIB + disp32编码。(当然,使用编码没有基址寄存器和索引寄存器的SIB字节。有趣的是:在这种情况下,比例字段未使用,但AMD64选择不对这些位进行任何操作)。

rbp not allowed as SIB base?详细介绍了ModRM编码中的“转义码”,它可以编码像SIB存在或无基址寄存器这样的特殊情况。

请注意,从RIP解码的偏移实际上是您的数据放置的绝对静态地址(在64位代码中),0x6000e5
该注释是反汇编器向您显示有效的绝对地址;RIP相对寻址从指令后的字节开始计数,即下一条指令的开头。

movabs 0x4b8c289006000e5,%eax

当目标寄存器为EAX时,汇编器(在32位模式下)选择更短的mov编码,使用没有ModR/M字节的A1 disp32从32位绝对地址加载eaxIntel手册将其称为moffs(内存偏移),而不是有效地址。

在x86-64模式下,该操作码需要一个64位绝对地址。(并且是唯一能够从64位绝对(非RIP相对)地址加载/存储而无需首先将地址放入寄存器的指令)。因此,解码将下一条指令的一部分作为64位地址的一部分消耗掉,这就是地址中的一些高字节来自的地方。0x6000e5在低32位中是正确的,并且是它作为32位机器代码解码的方式。


[bits 32]更改为[bit 64]

请参见如果在64位代码中使用32位int 0x80 Linux ABI会发生什么?

如果您不打算使用本机64位系统调用,则最好构建32位可执行文件。使用nasm -felf32,并使用gcc -m32 -nostdlib -static进行链接。


顺便提一下,您可以使用gdb的set disassembly-flavor intel命令从disas中获取Intel语法反汇编,并从layout reg模式下的反汇编窗口中获取。 - Peter Cordes

1
问题可能是str_hello_length的偏移量大于32位。IA-32不支持大于32位的位移。解决方法是使用RIP相对寻址,这通常是正确的前提假设,即RIP和您要访问的地址之间的距离适合32位。在这种情况下,基数是RIP,索引是指令长度,因此如果指令已经有了基数或索引,则不能使用RIP相对寻址。
让我们检查您的各种尝试:
str_hello_length equ $-str_hello
...
...
mov edx, str_hello_length

这里没有内存访问,只有一个简单的立即移动操作,因此根本没有寻址。

接下来:

mov eax, str_hello_length       
mov edx, [eax]  ; count

现在,第一条指令是立即移动,它仍然不是内存访问。第二条指令具有内存访问,但它使用eax作为基础,并且没有位移。当有位移时,RIP相对才相关,因此这里没有RIP相对。

最后:

str_hello_length db $-str_hello
...
...
mov edx, [str_hello_length]     ; of course, without the brackets it'll load the address, which I don't want. I want the value stored at that address

在这里,您使用str_hello_length作为位移量。如上所述,这将导致RIP相对寻址。


哎呀,忘了提到我使用的是AMD64处理器,虽然我不确定这是否有影响。谢谢回复!我会研究一下的。 - Mathmagician
1
不加上 default rel,在 x86-64 模式下汇编的 mov edx, [str_hello_length] 仍然会使用 32 位绝对寻址。 - Peter Cordes

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