6
\$\begingroup\$

According to this question, I have made an agreement with Peter Cordes to create such a question here.

I think that I should prepare the tutorial using the modern technologies as the Continuous Integration (hereinafter referred to as "CI").

Here is the public repo (branch tests) and here is the successful CI-build with the tests.

The tests were written in C#/.NET Core, using the MSTest framework, on Ubuntu (CI also uses Ubuntu docker image).

The source code may be found here.

It was divided into 4 files:

  1. functions.asm, the main source, which is working with the 64-bit syscalls
  2. sys_write.asm, the program, which using the SYS_WRITE syscall
  3. sys_write.loop.asm, the program, which using the SYS_WRITE syscall repeatedly (10 times)
  4. sys_read.asm, the program, which is using the SYS_READ syscall

The corresponding tests (in C#) are located here.

Main source file (functions.asm):

SECTION .data
inputTemplate  db  "User input: ", 0h
maxBytesToRead equ 255

sprint:
    push rdx
    push rdi
    push rax
    push rsi

    mov  rax, rsi
    pop  rsi
    jmp  next_char
    ret

sprint_linefeed:
    call sprint

    push rsi
    mov  rsi, 0Ah
    push rsi
    mov  rsi, rsp

    call sprint
    pop  rsi
    pop  rsi
    ret

read_input:
    push rsi
    call kernel_fn_read_input
    mov  r8 , rsi
    mov  rsi, inputTemplate
    call sprint
    mov  rsi, r8
    call sprint
    pop rsi
    ret

quit:
    call kernel_fn_quit
    ret

next_char:
    cmp byte [rax], 0
    jz  count_length
    inc rax
    jmp next_char

count_length:
    sub rax, rsi
    mov rdx, rax
    jmp kernel_fn_print_string

kernel_fn_print_string:
    mov rax, 1
    mov rdi, 1
    syscall

    pop rax
    pop rdi
    pop rdx
    ret

kernel_fn_read_input:
    push rax
    push rdx
    push rdi

    mov rax, 0
    mov rdx, maxBytesToRead
    mov rdi, 0
    syscall

    pop rdi
    pop rdx
    pop rax
    ret

kernel_fn_quit:
    push rax
    push rdi

    mov rax, 60
    mov rdi, 0
    syscall

    pop rdi
    pop rax
    ret

Here, I'm providing the several functions for handling the SYS_WRITE and SYS_READ syscalls in 64-bit mode. The 1st remark, which I can make for myself, that I didn't use the namespaces, which act like the private methods in high-level languages, like:

subroutineA:
    mov     rcx, firstMsg
    call    sprint
    jmp     .finished 

.finished:
    mov     rax, secondMsg
    ...

Peter has asked me:

Why you designed it that way (with push / pop of all registers you use, instead of letting it clobber rax, rcx, rdx, rsi, rdi, and r8-r11 like the x86-64 SystemV ABI allows for the standard function calling convention?

My answer:

I'm pushing it to the stack for safe keeping. When the function has finished it's logic, these registers should have their previous values, restored via the POP instruction. I'm achieving the result, that any values in the registers will be the same before and after. Also, I didn't read the SystemV ABI at that moment, but reading right now.

I'm new to x86/assembler/NASM world, so I want to hear your constructive critique, because later I shall post an answer to Peter Cordes' question in beginner-friendly manner.

Intended purpose of the future tutorial:

The simple application with the CI service, which may help the newbie to learn the correct use of the int 0x80 (for 32-bit) & syscall (for 64-bit) Linux ABI, as I promise Peter. That's why CI-service IS IMPORTANT, because it will show the beginner the conflict/exception. I want to prepare the failure tests, which will show the beginner, what happens if to use the 32-bit int 0x80 Linux ABI in 64-bit code with the deep diagnostics. Repo is public, so the beginner may execute the job and view the results in live mode.

\$\endgroup\$
  • \$\begingroup\$ Updated my answer; I knew there was something I forgot to mention but couldn't remember what. It's that sys_write can return early, so you should check the return value and retry unless this is definitely for toy programs only and you don't care about large strings. \$\endgroup\$ – Peter Cordes Nov 16 '17 at 4:21
  • \$\begingroup\$ @PeterCordes First of all, I want to express my gratitude to you, because you have provided the big & constructive answer with a lot of details. Right now, I'm reading the System V ABI doc (which consists of 124 pages) & also I'm reading with comprehension you answer. Also, I want to admit, that there will be other answer FROM ME, but much more constructive & meaningful, I still want to provide the tutorial for the beginners, but it must be well done in all meanings (NASM, CI, examples, usefulness). Thanks a lot, Peter. \$\endgroup\$ – Neverlands Nov 16 '17 at 11:57
  • \$\begingroup\$ Wow, I'd forgotten how many pages the ABI doc was (the current version, draft 0.99.8 r252, is 150 pages). The actual calling-convention parts are much shorter, and even then you usually don't need the details on packing structs/unions into registers unless you pass/return structs by value. The debug-info chapter is best left to the compiler; you can write that stuff by hand, but it's way more work than it's worth. It's interesting to have some idea how various things work, though, like stack unwinding. The ILP32 section documents the x32 ABI (32-bit pointers in long mode), skippable. \$\endgroup\$ – Peter Cordes Nov 16 '17 at 18:13
7
\$\begingroup\$

When the function has finished it's logic, these registers should have their previous values

The normal x86-64 System V calling convention allows functions to clobber RAX, RCX, RDX, RSI, RDI, and R8-R11. Most of these registers are also used for passing args to functions, or returning them (RAX and sometimes RDX).

For a more readable guide to calling conventions, see Agner Fog's calling conventions PDF which covers the differences between calling conventions on Windows/non-Windows and how to deal with it. (His optimization manuals are essential reading, too if you want to write efficient asm.)

Also, the standard calling convention passes the first arg in rdi. You seem to be using rsi. This needs to be documented with a comment. (Your whole code is under-commented, even though most of it is straightforward).

All libc functions like strlen follow the normal calling convention, so writing code that takes advantage of the fact that your functions don't clobber some registers requires remembering the non-standard calling convention they use.

Using a non-standard calling convention that doesn't clobber any registers might be good for some obscure use-cases like optimizing for code-size in a function with many callers, or for a debug-print function. You're making your functions bloated by forcing them to save/restore all registers. Also, restoring even rax stops you from returning an error code (possible if stdout is closed). I think your function is intended for use in toy programs that don't check for errors, though. But still, allowing your functions to clobber rax is pretty sensible.

BTW, in most stack-args calling conventions (including args that don't fit in registers in the x86-64 System V convention used on Linux), functions are allowed to modify their args on the stack. So it's not weird that functions are allowed to clobber the registers their args are in.

Also beware that inserting a debug-print function call isn't necessarily safe even if it restores all registers. x86-64 System V defines a red zone of 128B below rsp that a function can assume isn't clobbered asynchronously (by signal handlers if you have any), so a function might have valuable data stored at [rsp - 8] or something. Inserting a debug-print function call into a function that was previously a leaf function might break it (because call pushes a return address, overwriting memory below the old rsp). This is why you should just use a debugger instead of writing debug-print functions.

Anyway, if you insist on save/restoring all registers, don't forget that syscall itself clobbers rcx and r11. See What are the calling conventions for UNIX & Linux system calls on i386 and x86-64?. Or maybe you're intentionally not saving r8-r11? read_input writes r8 without saving it first. That's fine, you can use whatever non-standard calling convention you want, but document it with comments.

Also, implement it efficiently: in sprint, you push / pop RSI around a mov rax, rsi. You're only reading it, which doesn't modify rsi. Also, you should wait until the end of the function to restore RSI. If you're going to eventually save/restore registers inside your function, you might as well do it early.


Keep read-only string constants in section .rodata

section .data is for modifiable strings that go in read-write memory. Use section .rodata for your inputTemplate.


Indentation / formatting style:

It's nice to indent the operands to a consistent column, so different length mnemonics don't make your code as ragged. I've been doing this in code blocks in this answer.

Also don't forget to use : after labels even in .data / .rodata sections.


Make function definitions contiguous and use local labels for function internals.

next_char is not a usable entry-point to your code. It's part of sprint. So what you've done is put the first part of sprint early in memory, then code for another function, then the rest of sprint. This is not efficient for hardware to run (I-cache locality and a jmp instruction to reach the 2nd half of the function), or easy for anyone to read.

Putting a ret after a jmp is particularly silly:

jmp  next_char
ret

because nothing jumps back to execute the ret instruction. Anyway, much better to just put the next_char code right below the first part of sprint so execution falls into it.

Similarly, sys_exit is guaranteed not to return, so there's no point putting some pop and ret instructions after it. Or push instructions before it.

If some of the blocks were helper functions, it could make some sense, but they're not. (Although sprint_linefeed should use kernel_fn_print_string, see below. Except that you can't really use it because it pops more than it pushes. Having the save/restore built-in to blocks of code makes it hard to reuse them.)

For labels internal to a function, use .name instead of name to give it local scope.

You don't need to jmp over blank lines in the source

Execution continues to the next instruction on its own. This is an example:

count_length:
    sub rax, rsi
    mov rdx, rax
    jmp kernel_fn_print_string

kernel_fn_print_string:
    mov rax, 1
    ....

The jmp instruction is completely useless and a waste of space. You should have done this:

.count_length:
    sub    rax, rsi
    mov    rdx, rax          ; should have used rdx as the loop counter in the first place to save this mov
.kernel_fn_print_string:
    mov    eax, 1
    ....

Use more efficient instructions

mov eax,1 is smaller than mov rax,1, but exactly equivalent. NASM will actually optimize it to mov eax,1, but the other NASM-syntax assembler (YASM) won't, and will emit the 7-byte mov r/m64, imm32 form of the instruction (with a REX prefix) instead of 5-byte mov eax, imm32.

xor eax,eax is the most efficient way to zero eax. Use that instead of mov eax, 0.

Use push imm8 for small constants. mov rsi, 0Ah / push rsi can be just push 0xa. (I prefer C-style 0xDEADBEEF hex constants, not MASM-style trailing-h). sprint_linefeed is a good example of how bloated your calling convention forces your code to be.

sprint_linefeed:
    call   sprint

    push    rsi          ; this and the 2nd pop aren't needed if you can clobber caller's RSI
    push    0Ah
    mov     rsi, rsp
    call   sprint
    pop     rsi          ; clear the newline from the stack
    pop     rsi          ; restore RSI
    ret

Of course, you know you want to sys_write exactly one byte, so could just do that with a few instructions instead of calling sprint on a known-length string. Or you could push the right registers and jmp to kernel_fn_print_string directly. But that doesn't work because you need it to return to your code to pop rsi, and you can't call it because it pops more than it pushes before running a ret.

I guess you could make sprint do extra work (like save/restore rsi) so you could use it from other functions that use just the tail of the function to restore rsi and return. This whole thing is a mess, and without knowing what you're aiming for (simple code, small code-size, fast?) I have no idea what to recommend.

Use do{}while() loop structure in asm whenever possible.

So there's one taken conditional branch at the bottom instead of a not-taken branch at the top and a jmp at the bottom. (Also, prefer calling strlen because it's a lot faster than a naive byte-at-a-time loop for strings more than a few bytes long. glibc's strlen uses SSE2 pcmpeqb to look for the zero byte.)

.next_char:
    inc     rdx
    cmp     byte [rdx], 0
    jnz     .next_char

When a loop might need to run zero times, you can put a test/branch outside the loop. But strlen always looks at at least one byte.


Use global to export functions in the symbol table

global sprint makes it possible to use extern sprint and call sprint from a separately-assembled file.

Align your function entry-points for performance

If you're really going for code-size, then sure leave everything packed. But normally you want to align 16 before a function so there's a full fetch-block of code to decode. Some CPUs may do a 16B-aligned fetch that contains the target address when jumping, and if the branch target is near the end of that 16B then there are only a couple bytes to decode in the first cycle.


Comment your code, especially any magic numbers.

mov  eax, 1       ; __NR_write
mov  edi, 1       ; stdout
syscall           ; sys_write(1, buf, len)

You can optimize by using mov edi, eax instead of another mov-immediate for a constant that happens to be the same.

read_input requires a fixed-size 255 byte buffer instead of having the caller pass a size?!?

This is just weird. Write a function with 2 args: pointer + length, like a normal person.

read_input assumes that it can strlen() the buffer

read_input uses sprint to print the buffer as an implicit-length string. But sys_read doesn't append a trailing zero, so this is only safe if the caller zeroed the buffer so there's a terminating zero somewhere.

If your intent was to print was you got from sys_read, you should use the length return value in rax instead of examining memory. If your intent was to print out the buffer as an implicit-length string, then document that carefully (because it's weird).

Things would be simpler if you wrote a wrapper function that printed explicit-length strings. You could use that here, and from sprint_linefeed.

Return the sys_read result from read_input

Otherwise the caller can't tell how many bytes were read. The input bytes could be the same as what was already in the buffer. (Even on an interactive TTY, a user can type ^V to escape any literal byte including a zero byte.) (See this SO Q&A for example.


sys_write may return early for very large buffers

Nothing guarantees that sys_write will write all of the characters you pass it. For small strings this is usually fine, but see When does the write() system call write all of the requested buffer versus just doing a partial write?. e.g. if writing to a pipe, you're unlikely to be able to write more than the size of the kernel's pipe buffer in one system call.

If you want to be robust, check the sys_write return value to see if it's == the length, and if not advance the pointer and decrease the length and try again. (If the return value wasn't an error code: -4096 to -1).


My version of your code

SECTION .rodata
inputTemplate:  db  "User input: ", 0h

;;maxBytesToRead equ 255

;;; void sprint(char *str) // in RSI
;;; {  sys_write(stdout, str, strlen(buf); }

;;; clobbers RAX and R11, everything else unmodified
;;; doesn't attempt to retry on interrupted or incomplete writes.

;;; returns sys_write return value in RAX unless string length was zero, then leaves RAX unmodified.

ALIGN 16
global sprint
sprint:
    cmp     byte [rsi], 0  ; don't even make a syscall at all with zero length
    jz      .no_print

    push    rdx
    push    rdi
    mov     rdx, rsi
.next_char:              ; first byte already checked earlier
    inc     rdx
    cmp     byte [rdx], 0
    jnz     .next_char

    sub rdx, rsi          ; string length (excluding terminator)

 kernel_fn_print_string:
    push    rcx          ; syscall clobbers RCX and R11.

    mov     eax, 1       ; __NR_write
    mov     edi, eax     ; stdout = 1 as well
    syscall              ; sys_write(stdout, buf, rdx)

     ;; TODO: check return value and retry if not all the bytes were written.

    pop     rcx
    pop     rdi
    pop     rdx
.no_print:
    ret

ALIGN 16
global sprint_linefeed
;;; sprint(rsi);  sprint("\n");
sprint_linefeed:
    call    sprint

    ; calling  kernel_fn_print_string  is inconvenient here because it wants to pop more than it pushes.
    ; could just inline that code
    ; or for small strings, would be more optimal to copy to a local buffer and append the newline to make one sys_write() call
    push    rsi
    push    0xa          ; '\n'
    mov     rsi, rsp
    call sprint
    pop     rsi          ; pop the newline
    pop     rsi          ; restore caller's RSI
    ret

ALIGN 16
global  read_input
;;; size_t read_input(buf=RSI, len=RDX)
;;; { length = sys_read(0, buf, len);
;;;   print it out
;;;   return length;
;;;  }
;;; clobbers RAX, R8-R11
read_input:
    push    rdi
    push    rcx          ; syscall clobbers RCX and R11

    xor     eax, eax     ; __NR_read
    xor     edi, edi     ; stdin = 0
    ; rsi = buf
    ; rdx = len
    syscall              ; Linux syscalls preserve everything but RCX and R11

    ;; TODO: check for error (rax = -1 to -4095)
    mov     r8, rax      ; save write return value
    mov     r9, rsi

    mov     esi,  inputTemplate    ; static data is in the low 32 bits
    call    sprint       ; print constant string (TODO: length is known, don't waste time on strlen)
    mov     rsi, r9
    call    sprint       ; length is known here, too, if buffer was zeroed before.

    pop     rcx
    pop     rdi
    mov     rax, r8
    mov     rsi, r9      ; restore caller's RSI

    ret

ALIGN  8        ; tiny function, almost fits in 8 bytes
global quit
quit:
kernel_fn_quit:
    mov   eax, 60       ; __NR_exit
    xor   edi, edi
    syscall             ; sys_exit(0)

As I said earlier, this is still a mess. IDK what to recommend because IDK what this code is supposed to be useful for.

This still has some code-paths that save / restore and then save/restore the same register twice, e.g. sprint_newline, because it uses sprint internally multiple times. If you care more about code-size than speed, then it can make sense to do it that way.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.