写函数' print_string'它接受指向以null结尾的字符串的指针并将其打印到stdout

时间:2018-01-14 16:01:49

标签: assembly nasm x86-64

我目前正在阅读Zhirkov的书,"低级编程"为了自学。

我被困在第二章的结尾,即任务。我编写了第一个函数string_length,它接受​​一个指向字符串的指针并返回它的长度。我还创建了一个测试print_str函数,它打印出一个预定义的字节串。

我无法弄清楚如何编写作者定义的print_stringprint_string: accepts a pointer to a null-termianted string and prints it to stdout

section .data
string: db "abcdef", 0

string_str:
    xor rax, rax
    mov rax, 1        ; 'write' syscall
    mov rdi, 1        ; stdout
    mov rsi, string   ; address of string
    mov rdx, 7        ; string length in bytes
    syscall
  ret

string_length:
    xor rax, rax
    .loop
        cmp byte [rdi+rax], 0    ; check if current symbol is null-terminated
        je  .end                 ; jump if null-terminated
        inc rax                  ; else go to next symbol and increment
        jmp .loop
    .end
        ret                      ; rax holds return value

section .text

_start:
    mov rdi, string   ; copy address of string
    call print_str

    mov rax, 60
    xor rdi, rdi
    syscall

到目前为止,我有:

print_string:
    push rdi           ; rdi is the argument for string_length
    call string_length ; call string_length with rdi as arg
    mov rdi, rax       ; rax holds return value from string_legnth
    mov rax, 1         ; 'write' syscall

这是我更新的print_string函数,有效。有点。它将字符串打印到stdout,但后来我遇到了:illegal hardware instruction

print_string:
    push rdi               ; rdi is the first argument for string_length
    call string_length
    mov rdx, rax           ; 'write' wants the byte length in rdx, which is
                           ; returned by string_length in rax
    mov rax, 1             ; 'write'
    mov rdi, 1             ; 'stdout'
    mov rsi, string        ; address of original string. I SUSPECT ERROR HERE
    syscall
  ret

1 个答案:

答案 0 :(得分:2)

我认为此解决方案的最新版本是:

print_string:
    push rdi               ; rdi is the first argument for string_length
    call string_length
    mov rdx, rax           ; 'write' wants the byte length in rdx, which is
                           ; returned by string_length in rax
    mov rax, 1             ; 'write'
    mov rdi, 1             ; 'stdout'
    mov rsi, string        ; address of original string. I SUSPECT ERROR HERE
    syscall
  ret
  1. 为什么需要push rdi
  2. 通过调用约定[1]函数接受rdirsi等中的参数。它还保证了一些寄存器(rbprbx ,如果您调用另一个函数然后返回,则不会更改r11 - r15)。其他寄存器可以更改,rdi也可以。

    push rdi的目的是保存rdi以供日后使用,因为string_length可以根据需要重写其值。拿走它,string_length仍然有效,但你可能永远丢失字符串起始地址。

    因此,该指令与将参数传递给string_length无关。

    函数很少从堆栈中获取参数。它发生在例如当有超过6个整数/指针参数,或者参数很大时(例如256 bytes 宽)。

    1. 为什么它适用于pop rsi
    2. 让我们以这种方式改变解决方案:

      `

      print_string:
              push rdi               ; !!! save rdi to stack
              call string_length
              mov rdx, rax           ; 'write' wants the byte length in rdx, which is
                                     ; returned by string_length in rax
              mov rax, 1             ; 'write'
              mov rdi, 1             ; 'stdout'      
              pop rsi                ; !!! what was saved in stack is moved into rsi
              syscall
            ret
      

      我们刚刚恢复了已保存的字符串地址并将其写入rsi。这是一件好事,因为write系统调用期望rsi完全保留它。

      1. 为什么您的解决方案在没有pop rsi的情况下崩溃?
      2. 要理解它,让我们修改callret的工作方式。

        当调用print_string时,紧跟在call print_string之后的指令的地址放在堆栈顶部。此地址称为返回地址

        另一方面,ret将堆栈顶部的值弹出到rip,允许我们从保存的点继续执行。

        因此,将堆栈指针恢复为" vanilla"是非常重要的。 state,以便在执行ret时,它是放入rip的返回地址。

        在一个push和零pop的示例解决方案中,当ret执行print_string时,堆栈会保留这些值:

        |                  ...                           |
        |        ^ stack grows this way ^                |
        |                  ...                           | 
        | string starting address, saved from rdi.       | <- rsp
        | return address, to the caller of print_string. |
        |                  ...                           |
        

        执行ret时,由push rdi保存的字符串起始地址将移至rip,CPU将开始执行此地址的指令。显然,这给我们带来了好处。添加pop rsi后,执行ret时堆栈中不会存储额外信息,因此执行将按预期进行。

        您当然可以手动操作rsp,就像设置和恢复堆栈帧并使用rbpret之前恢复堆栈库一样。你会在第14章看到你做了很多。

        请注意,关于您的特定版本不会覆盖rdi的争论可能听起来令人信服。但是调用约定的目的是让程序员可以自由地改变任何功能并确保它不会干扰呼叫者&#39;关于哪些寄存器可以改变以及哪些寄存器不能改变的假设。所以,是的,在特定的情况下,这是有效的,但即便如此,它也使您无法自由更改string_length实施。

        [^ 1]:程序员和编译器编写者之间的明确协议,描述了传递参数的位置,哪些寄存器可以被破坏等等。在本书中,使用了GNU / Linux原生的调用约定。它在System V Application Binary Interface

        中有详细描述
相关问题