如何在 x86 程序集中获取长字符串的长度以在断言上打印

How to get length of long strings in x86 assembly to print on assertion

我正在尝试构建一个将文件读入内存的 x86 程序。它使用了一些不同的系统调用,并且会扰乱内存等。里面有很多东西要弄清楚。

为了简化调试和解决这个问题,我想添加 assert 语句,如果不匹配,它会打印出一条很好的错误消息。这是学习汇编的第一步,因此我可以在操作后打印放置在不同寄存器等上的数字和字符串。然后我可以将它们打印出来并在没有任何花哨工具的情况下进行调试。

想知道是否有人可以帮助我在 NASM 中为 Mac x86-64 写一个 ASSERTPRINT。到目前为止我有这个:

%define a rdi
%define b rsi
%define c rdx
%define d r10
%define e r8
%define f r9
%define i rax

%define EXIT 0x2000001
%define EXIT_STATUS 0

%define READ 0x2000003 ; read
%define WRITE 0x2000004 ; write
%define OPEN 0x2000005 ; open(path, oflag)
%define CLOSE 0x2000006 ; CLOSE
%define MMAP 0x2000197 ; mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t offset)

%define PROT_NONE 0x00 ; no permissions
%define PROT_READ 0x01 ; pages can be read
%define PROT_WRITE 0x02 ; pages can be written
%define PROT_EXEC 0x04 ; pages can be executed

%define MAP_SHARED 0x0001 ; share changes
%define MAP_PRIVATE 0x0002 ; changes are private
%define MAP_FIXED 0x0010 ; map addr must be exactly as requested
%define MAP_RENAME 0x0020 ; Sun: rename private pages to file
%define MAP_NORESERVE 0x0040 ; Sun: don't reserve needed swap area
%define MAP_INHERIT 0x0080 ; region is retained after exec
%define MAP_NOEXTEND 0x0100 ; for MAP_FILE, don't change file size
%define MAP_HASSEMAPHORE 0x0200 ; region may contain semaphores

;
; Assert equals.
;

%macro ASSERT 3
  cmp %1, %2
  jne prepare_error
prepare_error:
  push %3
  jmp throw_error
%endmacro

;
; Print to stdout.
;

%macro PRINT 1
  mov c, getLengthOf(%1) ; "rdx" stores the string length
  mov b, %1 ; "rsi" stores the byte string to be used
  mov a, 1 ; "rdi" tells where to write (stdout file descriptor: 1)
  mov i, WRITE ; syscall: write
  syscall
%endmacro

;
; Read file into memory.
;

start:
  ASSERT PROT_READ, 0x01, "Something wrong with PROT_READ"

  mov b, PROT_READ
  mov a, PROT_WRITE
  xor a, b

  mov f, 0
  mov e, -1
  mov d, MAP_PRIVATE
  mov c, a
  mov b, 500000
  mov a, 0
  mov i, MMAP
  syscall
  PRINT "mmap output "
  PRINT i ; check what's returned
  PRINT "\n"
  mov e, i

  mov b, O_RDONLY
  mov a, "Makefile"
  mov i, OPEN
  syscall
  mov a, i

  mov b, e
  mov i, READ
  syscall

;
; Exit status
;

exit:
  mov a, EXIT_STATUS ; exit status
  mov i, EXIT ; syscall: exit
  syscall

throw_error:
  PRINT pop() ; print error or something
  jmp exit

mov rsi, "abcdefgh" 是字符串 contents 的 mov-immediate,而不是指向它的指针。如果您这样做,它只会作为立即数存在。

您的宏需要切换到 .rodata 并返回以将字符串放入内存;也许您可以使用 NASM 宏将它变成 push-immediate 的序列放到堆栈上,但这听起来很难。

所以你可以使用通常的msglen equ $ - msg来获取长度。 (实际上使用 NASM 本地标签,因此宏不会产生冲突)。


参见 NASM - Macro local label as parameter to another macro 几周前我在其中基本上写了这个答案。但不完全是重复的,因为它没有使用字符串作为立即数的错误。

NASM 让宏切换部分然后 return 到它们扩展的任何部分的机制是让 section foo 定义一个宏 __?SECT?__ 作为 [SECTION foo]。请参阅手册和上面链接的问答。

    ; write(1, string, sizeof(stringarray))
    ; clobbers: RDI, RSI, RDX,   RCX,R11 (by syscall itself)
    : output: RAX = bytes written, or -errno
%macro PRINT 1
[section .rodata]                ; change section without updating __?SECT?__ macro
;; NASM macro-local labels
    %%str    db  %1          ; put the string in read-only memory
    %%strln  equ $ - %%str   ; current position - string start

__?SECT?__                       ; change back to original sectoin
  mov     edx, %%strlen           ; len
  lea     rsi, [rel %%str]        ; buf = the string.  (RIP-relative for position-independent)
  mov     edi, 1                  ; fd = stdout
  mov     eax, WRITE
  syscall
%endmacro

这不会尝试合并相同字符串的重复项。对同一消息多次使用它会效率低下。这对调试无关紧要。

我可以将你的 %defines 留给 RDI,让 NASM 将 mov rdi, 1(7 个字节)优化为 mov edi, 1(5 个字节)。但是 YASM 不会这样做,所以如果您关心任何使用 YASM 构建您的代码的人,最好明确说明。

我使用了 RIP-relative LEA,因为这是在 position-independent 代码中将静态地址放入寄存器的最有效方法。在 Linux non-PIE 可执行文件中,使用 mov esi, %%str(5 个字节并且可以 运行 在任何端口上,超过 LEA)。但是在 OS X 上,可执行文件所在的虚拟基地址 mapped/loaded 总是在 2^32 以上,你永远不希望 mov r64, imm64 具有 64 位绝对地址。
参见 How to load address of function or label into register


在 Linux 上,其中 system-call 数字是小整数,您可以使用 lea eax, [rdi-1 + WRITE] 通过 3 字节指令执行 eax = SYS_write 而对于 mov 则为 5 .

call-number 常量的标准名称是 sys/syscall.h 中的 POSIX SYS_foo 或 [=25= 中的 Linux __NR_foo ].但是 NASM 不能 #include C 预处理器 #define 宏,所以你需要机械地将其中一个 headers 转换为 NASM 语法,例如用一些脚本。

或者如果手动定义名称,只需选择 %define SYS_write 1