【问题标题】:Cycle Through and Print argv[] in x64 ASM在 x64 ASM 中循环并打印 argv[]
【发布时间】:2016-09-09 15:43:56
【问题描述】:

我一直在研究 while 循环来遍历所有 CLI 参数。在研究仅打印 1 个元素的解决方案时,我注意到了一些事情;这就是导致我来到这里的思考过程。

我注意到,如果我执行lea 16(%rsp), %someRegisterToWrite,我就能够获取/打印 argv[1]。接下来我尝试了lea 24(%rsp), %someRTW,这让我可以访问 argv[2]。我一直向上看它是否会继续工作,并且确实如此。

我的想法是继续将 8 添加到 %someRTW 并增加一个“计数器”,直到计数器等于 argc。以下代码在输入单个参数时效果很好,但没有打印任何带有 2 个参数的参数,当我输入 3 个参数时,它将打印前 2 个参数,中间没有空格。

.section __DATA,__data
.section __TEXT,__text
.globl _main
_main:
    lea (%rsp), %rbx        #argc
    lea 16(%rsp), %rcx      #argv[1]
    mov $0x2, %r14          #counter
    L1:
    mov (%rcx), %rsi        #%rsi = user_addr_t cbuf
    mov (%rcx), %r10
    mov 16(%rcx), %r11      
    sub %r10, %r11          #Get number of bytes until next arg
    mov $0x2000004, %eax    #4 = write
    mov $1, %edi            #edi = file descriptor 
    mov %r11, %rdx          #user_size_t nbyte
    syscall
    cmp (%rbx), %r14        #if counter < argc
    jb L2
    jge L3
    L2:
    inc %r14                
    mov 8(%rcx), %rcx       #mov 24(%rsp) back into %rcx
    mov $0x2000004, %eax
    mov $0x20, %rsi         #0x20 = space
    mov $2, %rdx
    syscall
    jmp L1
    L3:
    xor %rax, %rax
    xor %edi, %edi
    mov $0x2000001, %eax
    syscall

【问题讨论】:

  • syscall clobbers rcx。 PS:学习使用调试器。
  • 还有%r11,OP 也使用它。
  • @PeterCordes 是但不是跨越syscall
  • 除了已经提到的 sys_write 系统调用不直接打印字符,它需要一个指向包含字符的缓冲区的指针。由于这个原因,第二个系统调用中的mov $0x20, %rsi 将不起作用。 RSI 需要是一个 pointer 指向包含空格的缓冲区,我不确定为什么要在执行 mov $2, %rdx 时使用该系统调用打印 2 个字符.
  • 您似乎也在尝试使用此mov 16(%rcx), %r11 sub %r10, %r11 确定命令行参数的长度。如果它试图获取字符串长度,它将不起作用,因为参数指针可能不在连续的位置。当您检索参数开头的地址时,您应该通过向下扫描字符串查找 NUL(\0) 字符来查找字符串长度。

标签: macos assembly 64-bit x86-64 att


【解决方案1】:

我将假设在 64 位 OS/X 上您正在组装和链接,以至于您有意绕过 C 运行时代码。一个示例是在没有 C 运行时启动文件和系统库的情况下进行静态构建,并且您指定 _main 是您的程序入口点。 _start 通常是进程入口点,除非被覆盖。

在这种情况下,64 位内核会将 macho64 程序加载到内存中,并使用程序参数和环境变量等设置进程堆栈。 Apple OS/X 进程堆栈状态在启动时与 第 3.4 节中的 System V x86-64 ABI 中记录的相同:

一个观察结果是参数指针列表以 NULL(0) 地址终止。您可以使用它来循环所有参数,直到找到 NULL(0) 地址作为依赖 argc 中的值的替代方法。


问题

一个问题是您的代码假定所有寄存器都保存在SYSCALL 中。 SYSCALL指令本身会破坏RCXR11的内容:

SYSCALL 在特权级别 0 调用操作系统系统调用处理程序。它通过从 IA32_LSTAR MSR 加载 RIP 来实现(在将 SYSCALL 之后的指令地址保存到 RCX 之后)。 (WRMSR 指令确保 IA32_LSTAR MSR 始终包含规范地址。)

SYSCALL 还将 RFLAGS 保存到 R11 中,然后使用 IA32_FMASK MSR(MSR 地址 C0000084H)屏蔽 RFLAGS;具体来说,处理器清除 RFLAGS 中与 IA32_FMASK MSR 中设置的位相对应的每个位

避免这种情况的一种方法是尝试使用 RCXR11 以外的寄存器。否则,您将不得不在 SYSCALL if 中保存/恢复它们,您需要保持它们的值不受影响。内核还将使用返回值破坏 RAX

Apple OS/X system calls 列表提供了所有可用内核函数的详细信息。在 64 位 OS/X 代码中,每个系统调用号都有0x2000000 added

在 64 位系统中,Mach 系统调用是肯定的,但前缀为 0x2000000 — 这清楚地将它们与前缀为 0x1000000 的 POSIX 调用区分开来并消除歧义


您计算命令行参数长度的方法不起作用。一个参数的地址不一定要放在前一个参数之后的内存中。正确的方法是编写从您感兴趣的参数开头开始并搜索 NUL(0) 终止字符的代码。


打印空格或分隔符的代码不起作用:

mov 8(%rcx), %rcx       #mov 24(%rsp) back into %rcx
mov $0x2000004, %eax
mov $0x20, %rsi         #0x20 = space
mov $2, %rdx
syscall

当使用sys_write 系统调用时,RSI 寄存器是一个指向字符缓冲区的指针。您不能传递像 0x20(空格)这样的立即数。您需要将空格或其他分隔符(如新行)放入缓冲区并通过 RSI 传递该缓冲区。


修改后的代码

这段代码借鉴了之前资料中的一些思路和额外的清理,将每个命令行参数(不包括程序名)写入标准输出。每个将由换行符分隔。 Darwin OS/X 上的换行符是 0x0a (\n)。

# In 64-bit OSX syscall numbers = 0x2000000+(32-bit syscall #)
SYS_EXIT  = 0x2000001
SYS_WRITE = 0x2000004

STDOUT    = 1

.section __DATA, __const
newline: .ascii "\n"
newline_end: NEWLINE_LEN = newline_end-newline

.section __TEXT, __text
.globl _main
_main:
    mov (%rsp), %r8             # 0(%rsp) = # args. This code doesn't use it
                                #    Only save it to R8 as an example.
    lea 16(%rsp), %rbx          # 8(%rsp)=pointer to prog name
                                # 16(%rsp)=pointer to 1st parameter
.argloop:
    mov (%rbx), %rsi            # Get current cmd line parameter pointer
    test %rsi, %rsi
    jz .exit                    # If it's zero we are finished

    # Compute length of current cmd line parameter
    # Starting at the address in RSI (current parameter) search until
    # we find a NUL(0) terminating character.
    # rdx = length not including terminating NUL character

    xor %edx, %edx              # RDX = character index = 0
    mov %edx, %eax              # RAX = terminating character NUL(0) to look for
.strlenloop:
         inc %rdx               # advance to next character index
         cmpb %al, -1(%rsi,%rdx)# Is character at previous char index
                                #     a NUL(0) character?
         jne .strlenloop        # If it isn't a NUL(0) char then loop again
    dec %rdx                    # We don't want strlen to include NUL(0)

    # Display the cmd line argument
    # sys_write requires:
    #    rdi = output device number
    #    rsi = pointer to string (command line argument)
    #    rdx = length
    #
    mov $STDOUT, %edi
    mov $SYS_WRITE, %eax
    syscall

    # display a new line
    mov $NEWLINE_LEN, %edx
    lea newline(%rip), %rsi     # We use RIP addressing for the
                                #     string address
    mov $SYS_WRITE, %eax
    syscall

    add $8, %rbx                # Go to next cmd line argument pointer
                                #     In 64-bit pointers are 8 bytes
    # lea 8(%rbx), %rbx         # This LEA instruction can replace the
                                #     ADD since we don't care about the flags
                                #     rbx = 8 + rbx (flags unaltered)
    jmp .argloop

.exit:
    # Exit the program
    # sys_exit requires:
    #    rdi = return value
    #
    xor %edi, %edi
    mov $SYS_EXIT, %eax
    syscall

如果您打算在不同的地方使用strlen 之类的代码,那么我建议您创建一个执行该操作的函数。为简单起见,我已将 strlen 硬编码到代码中。如果您希望提高 strlen 实现的效率,那么 Agner Fog 的 Optimizing subroutines in assembly language 是一个不错的起点。

这段代码应该编译并链接到一个没有C运行时的静态可执行文件,使用:

gcc -e _main progargs.s -o progargs -nostartfiles -static

【讨论】:

  • 我无法理解的部分是cmpb $0, -1(%rsi,%rdx)。我以if (%rsi[%rdx -1] == 0) 阅读此内容。对吗?
  • 这类似于将内存地址 rsi+rdx-1 处的字节与 0 进行比较。其中 RSI 是字符串的开始 RDX 是当前位置,-1 是返回一个。 -1 是因为我们在每个循环中首先增加 RDX,所以我们必须与前一个字节进行比较。这几乎就是我认为if (%rsi[%rdx -1] == 0) 的意思
【解决方案2】:

正如您已经正确理解的那样,堆栈上的第一个参数是参数的数量,第三个和以下是 cli-arguments。 第二个是程序的实际名称。 您不必关心 argc,因为您可以弹出堆栈直到值为零。一个简单的解决方案是:

add $0x10, %rsp
L0:
  pop %rsi
  or %rsi, %rsi
  jz L2
  mov %rsi, %rdi
  xor %rdx, %rdx
  L1:
    mov (%rsi), %al
    inc %rsi
    inc %rdx
    or %al, %al
  jnz L1
  ;%rdx - len(argument)
  ;%rdi - argument
  ;< do something with the argument >
  jmp L0
L2:

如果您想在每个参数后添加一个空格或换行符,只需将其打印出来 :)。

lea (newline), %rsi
mov $0x02, %rdx
mov STDOUT, %rdi
mov sys_write, %rax
[...]
newline db 13, 10, 0

我对 %rax 中的系统调用号有点困惑,但我猜它是 OSX 的东西? 正如 Jester 和 Peter Cordes 已经提到的,系统调用会覆盖寄存器: %rcx 带有返回地址 (%rip) 和 %r11 带有标志 (%rflags)。 我建议查看 intel x86_64 文档:http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-manual-325462.pdf

这段代码的另一件事:

jb L2
jge L3
L2:

argc 和 counter 是无符号的,所以这看起来更好一些,我猜:

jae L3

抱歉,如果代码不起作用。我通常使用 intel-syntax,但我没有测试它,但我相信你明白了 :)

【讨论】:

  • syscall-number in %raxx86-64 System V ABI 的标准,Linux 和 OS X(以及除 Windows 之外的所有系统)使用,当然每个操作系统都有自己的编号系统。有关链接,请参阅the x86 tag wiki。另外,那是您链接的一个 PDF 中的整个 3 卷集吗?如果我要直接链接 PDF,我会只链接 insn 集参考 (vol2)。
  • 由于您在分支之前有效地对计数器进行了预递增,因此您可能应该在退出循环后减少 %rdx,否则您的字符串长度将包括 NUL 终止字符。 newline db 13, 10, 0 在 GNU Assembler 中是 newline: .byte 13, 10, 0,但在现代 OS/X 换行符是 0x0a(13,10 是 Windows)。它可以被声明为 newline: .asciz "\n" 。 GNU 汇编器支持 C 风格的字符串。 .ascii 也可以工作,因为系统调用不需要尾随 \0 NUL 字符(当到达缓冲区中的 \0 时,sys_write 不会停止打印)
  • 如果您在之后从 %RSI 中减去 %RDI,您也可以在循环中消除 RDX 的使用循环(并减去一个)。这也会产生长度(没有 nul 终止符)。
  • 感谢这篇文章中的每一个人!很棒的对话,我学到了很多东西。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-12-03
  • 1970-01-01
  • 2021-01-01
  • 2021-08-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多