首页 > 解决方案 > NASM - 宏本地标签作为另一个宏的参数

问题描述

我正在尝试使用宏(如本教程所示)来打印字符串。该宏PRINT创建本地标签来定义字符串内容 ( str) 和长度 ( strlen),然后将这些作为参数传递给_syscall_write进行系统调用的第二个宏。

但是运行代码失败,我收到一条Segmentation fault (core dumped)消息。

我怀疑问题出在这个特定的行上,但我不明白为什么。

mov rsi, %1  ; str
mov rdx, %2  ; strln

这是完整的代码:

%macro PRINT 1

    ; Save state
    push rax
    push rdi
    push rsi
    push rdx

    %%str    db  %1, 0       ; arg0 + null terminator
    %%strln  equ $ - %%str   ; current position - string start

    ; Write
    _syscall_write %%str, %%strln

    ; Restore state
    pop rdx
    pop rsi
    pop rdi
    pop rax

%endmacro

%macro _syscall_write 2
    mov rax, 1
    mov rdi, 1
    mov rsi, %1  ; str
    mov rdx, %2  ; strln
    syscall
%endmacro


global _start


section .data

    SYS_EXIT   equ 60
    EXIT_CODE  equ 0


section .text

    _start:

        PRINT "Hello World!"


    exit:

        mov rax, SYS_EXIT
        mov rdi, EXIT_CODE
        syscall


这是目标文件的反汇编(来自已注释掉 push/pop 的版本)。

查看扩展的代码,我仍然看不出有什么问题。字节 0x0..0xC 看起来像乱码,但对应于Hello World!. 在 syscall 到 sys_write 之前,rax似乎rdi收到了 的预期值0x1rsi其值0x0指向字符串开始,rdx其值0xd是字符串长度 (12 + 1)...

Disassembly of section .text:

0000000000000000 <_start>:
   0:   48                      rex.W
   1:   65                      gs
   2:   6c                      ins    BYTE PTR es:[rdi],dx
   3:   6c                      ins    BYTE PTR es:[rdi],dx
   4:   6f                      outs   dx,DWORD PTR ds:[rsi]
   5:   20 57 6f                and    BYTE PTR [rdi+0x6f],dl
   8:   72 6c                   jb     76 <SYS_EXIT+0x3a>
   a:   64 21 00                and    DWORD PTR fs:[rax],eax

   d:   b8 01 00 00 00          mov    eax,0x1
  12:   bf 01 00 00 00          mov    edi,0x1
  17:   48 be 00 00 00 00 00    movabs rsi,0x0
  1e:   00 00 00
  21:   ba 0d 00 00 00          mov    edx,0xd
  26:   0f 05                   syscall

0000000000000028 <exit>:
  28:   b8 3c 00 00 00          mov    eax,0x3c
  2d:   bf 00 00 00 00          mov    edi,0x0
  32:   0f 05                   syscall

标签: assemblyx86macrosx86-64nasm

解决方案


rex.W gs ins是特权指令,并且在用户空间中存在错误。这是您的程序的第一条指令,从%%str db %1, 0宏中的扩展开始,而不更改部分。

不要将数据放在将作为指令执行的地方;用于section .rodata只读数据,(或.rdata在 Windows 上)然后切换回原始部分。

%macro PRINT 1
  ...
section .rodata 
    %%str    db  %1, 0       ; arg0 + null terminator
    %%strln  equ $ - %%str   ; current position - string start
section .text
  ... rest of the macro

这无条件地切换到.textsection,无论您在使用此宏时位于哪个部分(例如.text.cold或其他一些自定义部分)。

GAS 会让你.pushsection .rodata/.popsection在任何部分中正确扩展宏。NASM 有一个不同的机制来允许这个,它不会嵌套。 有关如何定义as的详细信息,请参阅NASM 手册,并且“原始指令”将在不这样做的情况下切换。section foo__?SECT?__[section foo][section bar]

切换回原始部分的更安全的版本

%macro PRINT 1
  ...
[section .rodata]        ; switch to .rodata without updating __?SECT?__
    %%str    db  %1, 0       ; arg0 + null terminator
    %%strln  equ $ - %%str   ; current position - string start
__?SECT?__               ; switch back to original section, likely [section .text]
  ... rest of the macro

%push [optional context-name] / %pop通过使用在上下文堆栈上保存/恢复,实际上可以嵌套切换部分的宏__?SECT?__,因此您可以使用用户级sectionnot [section]。但这可能与宏的使用不兼容,这些宏也是%define您希望稍后可见的东西。由于这在实践中不太可能需要,所以我没有尝试。(例如,您切换到 .rodata 并使用另一个宏,该宏本身会切换到其他位置。)我提到这一点主要是因为 GAS .pushsection/.popsection确实很容易嵌套。


如果您的任何静态数据每次都相同,您可以将其拉到宏之外,或者使用%ifndef/%define保护,以便第一次使用宏(在一个文件中)发出数据(带有不使用的普通标签%%, like debugprint_str) 供以后扩展参考。(请参阅在宏中定义变量会导致同一变量的多个定义)。这样做的好处是,如果宏使用零次,则数据根本不会成为您程序的一部分。但是对于宏参数字符串,除非您将字符串烘焙到符号名称中,否则您将无法在汇编时进行重复消除/折叠。


另请注意,equ指令不关心它们所在的部分(除非它们$在定义中使用)。所以strln需要与 相同的部分str,但SYS_EXIT与 无关section .data。它是一个汇编时常量,当您以这种方式使用它时,它会变成立即数。


mov r64, imm64将绝对地址放入寄存器是一种低效的方法。它需要在 PIE 可执行文件中进行加载时修复,并且比 position-independent 更长lea rsi, [rel %%str]。NASM 组装mov rsi, str成 10-byte mov r64, imm64,而 YASM 使用mov r/m64, sign_extended_imm32(它甚至在 PIE 可执行文件中不起作用)。 https://nasm.us/doc/nasmdo11.html#section-11.2

您也许可以编写一个使用%ifidn字符串相同条件的宏来检查rsi字符串 arg,在这种情况下什么都不做(指针已经在 RSI 中),否则使用lea rsi, [rel %%str]. 但是,这不适用于内存中的指针mov rsi, [rbx],但在哪里可以工作。取决于你希望你的宏有多花哨。您可能会在 arg 字符串%if中查找一个条件并使用而不是.[movlea


如果您想保存/恢复您破坏的所有寄存器,请记住它syscall本身会破坏 RCX(保存的 RIP)和 R11(保存的 RFLAGS)。

通常,您只需记录哪些注册了宏破坏器;这些都是 x86-64 System V 中的 call-clobbered 寄存器。但是如果您想要一个调试打印宏,您可能希望它保存/恢复所有内容?除了push/pop摧毁 RSP 以下的红色区域。

我不认为我曾经在 asm 中使用过调试打印,只是用调试器设置断点并点击“继续”以查看下一个断点。或者只是单步并观察寄存器值的变化,例如使用 GDB 的layout reg.


推荐阅读