【问题标题】:Distinguishing memory from constant in GNU as .intel_syntax在 GNU 中将内存与常量区分开来 .intel_syntax
【发布时间】:2020-10-17 07:03:21
【问题描述】:

我有一条用 Intel 语法编写的指令(使用 gas 作为我的汇编程序),如下所示:

mov rdx, msg_size
...
msg: .ascii "Hello, world!\n"
     .set msg_size, . - msg

但正如我所料,该 mov 指令正在组装到mov 0xe,%rdx,而不是mov $0xe,%rdx。我应该如何编写第一条指令(或msg_size 的定义)以获得预期的行为?

【问题讨论】:

  • 当我尝试这样做时,我得到undefined reference to `$msg_size'
  • 哦,对不起,我错过了关于英特尔语法的部分。在真正的 MASM 语法中,您不需要做任何事情。你可以试试OFFSET msg_size
  • 是的,这行得通,谢谢。我有点太习惯 nasm 了,我想……
  • @RossRidge 抱歉,还有一个问题。在lea <reg>, [<reg> + <constant>] 中有什么类似的工作原理?
  • 在这种情况下,这无关紧要。这无疑是一种位移。

标签: assembly x86-64 gnu-assembler intel-syntax


【解决方案1】:

使用mov edx, OFFSET symbol 获取符号“地址”作为立即数,而不是从它作为地址加载。这适用于实际标签地址以及您使用.set 设置为整数的符号。

对于 64 位代码中的 msg 地址(不是 msg_size 汇编时间常量),您可能需要
lea rdx, [RIP+msg] 用于静态地址不具有的 PIE 可执行文件' t 适合 32 位。 How to load address of function or label into register


在 GAS .intel_syntax noprefix 模式下:

  • OFFSET symbol 的工作方式类似于 AT&T $symbol。这有点像 MASM。
  • symbol 的作用类似于 AT&T symbol(即取消引用),用于未知符号。
  • [symbol] 在 GAS 和 NASM/YASM 中始终是有效地址,而不是直接地址。 LEA 不会从地址加载,但它仍然使用内存操作数机器编码。 (That's why lea uses the same syntax)。

对裸symbol的解释取决于声明的顺序

GAS 是一次性汇编器(回过头来填充 已知的符号值)。

它在第一次遇到该行时决定mov rdx, symbol 的操作码和编码。 较早的 msize= . - msg.equ / .set 将使其选择 mov reg, imm32,但稍后的指令将不可见。

未定义符号的默认假设是 symbol 是某个部分中的地址(就像您使用 symbol: 之类的标签定义它或从 .set symbol, . 中获得的一样)。而且因为 GAS .intel_syntax 类似于 MASM 而不是 NASM,所以将裸符号视为 [symbol] - 一个内存操作数。

如果您将.setmsg_length=msg_end - msg 指令放在文件顶部,在引用它的指令之前,它们将汇编为mov reg, imm32 mov-immediate。 (与 AT&T 语法不同,您总是需要一个 $ 来表示立即数,即使对于像 1234 这样的数字文字也是如此。)

例如:源代码和反汇编代码与objdump -dS 交错:
gcc -g -c foo.s 组装并用objdump -drwC -S -Mintel foo.o 反汇编(as --version = GNU 汇编器 (GNU Binutils) 2.34)。我们得到这个:

0000000000000000 <l1>:
.intel_syntax noprefix

l1:     
mov eax, OFFSET equsym
   0:   b8 01 00 00 00          mov    eax,0x1
mov eax, equsym            #### treated as a load
   5:   8b 04 25 01 00 00 00    mov    eax,DWORD PTR ds:0x1
mov rax, big               #### 32-bit sign-extended absolute load address, even though the constant was unsigned positive
   c:   48 8b 04 25 aa aa aa aa         mov    rax,QWORD PTR ds:0xffffffffaaaaaaaa
mov rdi, OFFSET label
  14:   48 c7 c7 00 00 00 00    mov    rdi,0x0  17: R_X86_64_32S        .text+0x1b

000000000000001b <label>:

label:
nop
  1b:   90                      nop

.equ equsym, . - label            # equsym = 1
big = 0xaaaaaaaa

mov eax, OFFSET equsym
  1c:   b8 01 00 00 00          mov    eax,0x1
mov eax, equsym           #### treated as an immediate
  21:   b8 01 00 00 00          mov    eax,0x1
mov rax, big              #### constant doesn't fit in 32-bit sign extended, assembler can see it when picking encoding so it picks movabs imm64
  26:   48 b8 aa aa aa aa 00 00 00 00   movabs rax,0xaaaaaaaa

使用mov edx, OFFSET msg_size 将任何符号(甚至是数字文字)视为立即数总是安全的,无论它是如何定义的。所以它和 AT&T $ 完全一样,只是当 GAS 已经知道符号值只是一个数字而不是某个部分中的地址时它是可选的。 为了保持一致性,最好始终使用OFFSET msg_size,这样您的代码就不会改变含义,如果未来的程序员移动代码,因此数据部分和相关指令不再是第一位的。 (包括那些忘记了这些与大多数汇编程序不同的奇怪细节的未来的你。)

顺便说一句,.set.equ 的同义词,还有 symbol=value syntax 用于设置与 .set 同义的值。


Operand-size:一般使用32位,除非某个值需要64

mov rdx, OFFSET symbol 将组装到 mov r/m64, sign_extended_imm32。除非它是负常数,而不是地址,否则您不希望它的长度很小(远小于 4GiB)。您也不希望movabs r64, imm64 获取地址;这是低效的。

在 GNU/Linux 下,将 mov edx, OFFSET symbol 写入位置相关的可执行文件是安全的,实际上您应该始终这样做或使用 lea rdx, [rip + symbol],除非您编写的代码将被加载到高 2GB 的虚拟地址空间(例如内核)中。 How to load address of function or label into register

另请参阅32-bit absolute addresses no longer allowed in x86-64 Linux?,了解有关 PIE 可执行文件是现代发行版中默认设置的更多信息。


提示:如果您知道 AT&T 或 NASM 语法,或 NASM 语法,请使用它来生成您想要的编码,然后使用 objdump -Mintel 反汇编以找出 .intel_syntax noprefx 的正确语法。

但这在这里没有帮助,因为反汇编只会显示像mov edx, 123 这样的数字文字,而不是mov edx, OFFSET name_not_in_object_file。查看gcc -masm=intel 编译器输出也有帮助,但编译器再次执行自己的常量传播,而不是使用符号作为汇编时常量。

顺便说一句,据我所知,没有任何开源项目包含 GAS intel_syntax 源代码。如果他们使用天然气,他们使用 AT&T 语法。否则他们使用 NASM/YASM。 (您有时还会在开源项目中看到 MSVC 内联汇编)。


AT&T 语法或[RIP + symbol] 的效果相同

这更加人为,因为您通常不会使用不是地址的整数常量来执行此操作。我在这里包含它只是为了展示 GAS 行为的另一个方面,具体取决于在其 1 次传递期间的某个点是否定义了符号。

How do RIP-relative variable references like "[RIP + _a]" in x86-64 GAS Intel-syntax work? - [RIP + symbol] 被解释为使用相对寻址到达symbol,而不是实际添加两个地址。但是[RIP + 4] 是字面意思,作为相对于该指令结尾的偏移量。

同样,当 GAS 到达引用它的指令时,它对符号的了解很重要,因为它是 1-pass。如果未定义,则假定它是普通符号。如果定义为没有关联部分的数值,则其工作方式类似于文字数字。

_start:
foo=4
jmpq *foo(%rip)
jmpq *bar(%rip)
bar=4

这汇编到第一次跳转与jmp *4(%rip) 从当前指令结束后的 4 个字节加载指针相同。但是第二次跳转使用bar 的符号重定位,使用RIP 相对寻址模式到达符号bar 的绝对地址,无论结果如何。

0000000000000000 <.text>:
   0:   ff 25 04 00 00 00       jmp    QWORD PTR [rip+0x4]        # a <.text+0xa>
   6:   ff 25 00 00 00 00       jmp    QWORD PTR [rip+0x0]        # c <bar+0x8> 8: R_X86_64_PC32        *ABS*

ld foo.o链接后,可执行文件有:

  401000:       ff 25 04 00 00 00       jmp    *0x4(%rip)        # 40100a <bar+0x401006>
  401006:       ff 25 f8 ef bf ff       jmp    *-0x401008(%rip)        # 4 <bar>

【讨论】:

  • “据我所知,没有任何开源项目包含 GAS intel_syntax 源代码。” 哇,真的吗?也许这就是你习惯的一切,但我讨厌 AT&T 的语法。如果我要花费任何精力编写/维护 GAS 内联汇编,我肯定更喜欢使用 Intel 语法。人们不这样做有什么技术原因吗?比如,GAS 对 Intel 语法有很大的限制吗? (我主要是 MSVC 人。MASM 语法很棒,但是无法为内联汇编指定输入参数,这使得它难以用于优化。)
  • @CodyGray:当我第一次自学 x86 asm 时,我习惯了 AT&T 语法,因为那是 gcc / objdump 使用的。直接操作数上的显式 $ 是一大优势。但是,一旦我意识到唯一真正的 insn 集引用是 Intel 语法,并且稍微习惯了这一点,我开始意识到它似乎更容易/更好阅读。 Dest 在左边,更好的内存操作数语法。我总是在 Godbolt 和类似的东西上使用 Intel 语法模式。我只在编写 gcc 错过优化错误报告时使用 AT&T 语法,因为这似乎是编译器开发人员选择的。
  • @Cody:就技术限制而言,我不这么认为。但它没有很好的记录。我似乎记得一个 SO 问题,其中有一些 GAS intel_syntax 是半模棱两可或限制性的,但我不记得问题的方式。我认为你想要 Intel 语法,不过,使用 NASM 或 YASM 通常会更好,所以中途使用 gas .intel_syntax 是不寻常的。
  • @CodyGray:哦,我只记得一个具体的缺点:gas .intel_syntax 仍然具有 AT&T 语法 design bug,某些形式的非交换 x87 insns(如 fdivrfdiv ) 被颠倒。我刚刚重新测试了这个,NASM fsubr st3, st0 是 objdump (AT&T) fsub %st,%st(3) 和 objdump -Mintel fsub st(3),st。 (注意这两种情况下的 fsub 助记符。)Agner Fog 的 objconv -fyasm 将其反汇编为 fsubr st3, st(0),并使用正确的助记符。对因兼容性原因无法修复的设计错误表示反对
  • @CodyGray:您根本不能将 YASM/NASM 用于内联汇编。使用 NASM/YASM 的 x264 和 x265 等项目将 asm 放在单独的文件中(并大量使用汇编器宏)。
猜你喜欢
  • 2022-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-18
相关资源
最近更新 更多