1

I am currently going through Zhirkov's book, "Low Level Programming" for the purpose of self-learning.

I am stuck on the end of chapter two, the assignment. I have written the first function, string_length, which accepts a pointer to a string and returns its length. I have also created a test print_str function which prints out a pre-defined string of bytes.

I cannot figure out how to write print_string as defined by the author: print_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

So far, I have:

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

This is my updated print_string function, which, works. Sort of. It prints the string to stdout, but then I am met with: 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
Jonathan Dewein
  • 984
  • 3
  • 16
  • 34
  • 1
    You wrote "I cannot figure out how to write `string_length`", but I think you meant `print_string`. – Peter Cordes Jan 14 '18 at 16:07
  • 1
    In your attempt at `print_string`, which arg register does `sys_write` want the length in? Hint: it's not `rdi`; look at your `string_str` block to see what goes in which registers. Perhaps it would help to look at a function that converts an integer to a base10 string and then `write`s that variable number of bytes: https://stackoverflow.com/questions/13166064/how-do-i-print-an-integer-in-assembly-level-programming-without-printf-from-the/46301894#46301894 – Peter Cordes Jan 14 '18 at 16:09
  • @Peter Corbes Ah, yes, of course! rdx is the length register, and the length is returned by the string_length function and stored in rax. So, I must use `mov rdx, rax`. Then, I can use `mov rax, 1; mov rdi, 1` to have my 'write' syscall and 'stdout' file descriptor. My remaining problem is `rsi`, which, according to the author is "source index in string manipulation commands". So, I think I need to use `rsi`, which...theoretically, holds the address of the original string? – Jonathan Dewein Jan 14 '18 at 16:43
  • @PeterCordes Ok, I added `pop rsi`, and now it works! I don't understand why, however. I think what's happening is that rsi is automatically set to hold the "source index" of the original string, which is set by `string_length`. So, by popping it off of the stack, I gain access to the address of the start of that original value. – Jonathan Dewein Jan 14 '18 at 17:03
  • 1
    `pop rsi` works because you're popping what you saved earlier with `push rdi` before calling your custom version of `strlen`. Functions are allowed to clobber their args in the normal calling convention, so it's correct for the caller to save/restore the function arg that it will need after the `call`. (Or you could have it take advantage of the fact it knows `string_length` doesn't modify `rsi` or `rdi` and `mov rsi, rdi` before the string-length loop. And you could count the length in `rdx` instead of `rax` if you're inlining the length loop). And yes, it's cheating to do `mov rsi, msg`. – Peter Cordes Jan 14 '18 at 19:57

1 Answers1

2

I assume your up-to-date version of this solution is:

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. Why push rdi is needed?

By the calling convention [1] functions accept their arguments in rdi, rsi etc. It also guarantees that some registers (rbp, rbx, r11-r15) will not be changed if you call another function and then return. Other registers can be changed, so can rdi.

The purpose of push rdi is to save rdi for later use, because string_length can rewrite its value according to its needs. Take it away, string_length still works, but you might lose the string starting address forever.

This instruction is thus irrelevant to passing arguments to string_length.

Functions rarely take arguments from stack. It happens e.g. when there are more than 6 integer/pointer arguments, or the arguments are huge (e.g. 256 bytes wide).

  1. Why does it work with pop rsi

Let's change the solution this way:

`

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

We have just restored a saved string address and wrote it into rsi. This is a good thing since the write system call expects rsi to hold exactly it.

  1. Why your solution crashed without pop rsi?

To understand it, let us revise how call and ret work.

When print_string is called, the address of the instruction immediately following call print_string is put on top of the stack. This address is called return address.

On the other hand, ret pops the value from the top of the stack into rip, allowing us to continue execution from the saved point.

Because of this, it is extremely important to restore the stack pointer to a "vanilla" state so that when ret is executed, it was the return address that is put into rip.

In your example solution with one push and zero pop, the stack holds these values when ret from print_string is executed:

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

When ret is executed, the string starting address, saved by push rdi, is moved into rip and the CPU starts executing instructions from this address on. Clearly, that brings us no good. After adding pop rsi, there is no extra information stored in stack when ret is executed, so the executions proceeds as it should.

You can of course manipulate rsp by hand, like when setting up and restoring stack frame and using rbp to restore stack base before ret. You will see that being done a lot in Chapter 14.


Note, that the argument about your specific version not overwriting rdi might sound convincing. But the purpose of the calling convention is to give programmers freedom of changing any function and being sure it won't interfere with the callers' assumptions about which registers can be changed and which can't. So yes, in a specific case this works, but even then it makes you unable to freely change string_length implementation.

[^1]: An explicit agreement between programmers and compiler writers, describing where to pass arguments, which registers can be clobbered etc. In this book, the calling convention native to GNU/Linux is used. It is described fully in System V Application Binary Interface

Igor Zhirkov
  • 303
  • 2
  • 8
  • It's Igor Zhirkov! Thank you so much for providing such a detailed explanation and walkthrough for me. I am greatly enjoying your book thus far, you have already taught me a lot! – Jonathan Dewein Jan 17 '18 at 23:19
  • 2
    @JonathanDewein you are very welcome! Do not forget to take a look at [errata](http://github.com/Apress/low-level-programming) from time to time :) – Igor Zhirkov Jan 18 '18 at 00:12