2
\$\begingroup\$

Although I've used assembly quite a bit for some simply things, I feel like I've never properly learnt how to write it idiomatically, by which I mean: which variables to use where, how best to structure loops and conditionals, how best/when to preserve registers over procedure calls, etc...

I wrote a small program which interfaces with the C standard library runtime, and will simply print the the amount of command line arguments followed by a list of all those arguments, for example:

$ ./sver Hello, this is a test!
The (6) arguments:
./sver
Hello,
this
is
a
test!

And here's the code itself:

    .global main
    .type main, @function

main:
    mov    %rsi, %r12

    mov    %rdi, %rsi
    lea    message(%rip), %rdi
    xor    %rax, %rax
    pushq  %rsi
    call   printf
    popq   %rsi


loop:
    cmp    $0, %rsi # If we've printed everything, then finish
    jz end

    mov    0(%r12), %rdi # Dereference r12 to get the current arg
    pushq  %rsi
    call   puts # Print one argument
    popq   %rsi

    add    $8, %r12 # Advance r12 to point at the next arg
    dec    %rsi
    jmp    loop
end:
    mov    $0, %eax
    ret

    .section .data
message:
    .string "The (%u) arguments:\n"

# Explicitly say that I don't want an executable stack, otherwise GCC complains
.section .note.GNU-stack,"",@progbits

I'm compiling this with GCC.

Some aspects specifically that I'm unsure about:

  • I'm using R12 to store a copy of RSI (which is just argv from C). I use it later when iterating to print all the arguments. However I feel like something about this is weird - is there a more idiomatic way of doing this?
  • Before/after calls to printf and puts I need to preserve the value of RSI, since I use it as the argument counter. To do this I push/pop it from the stack, but perhaps it would be better to store it in some callee-saved register instead? But it still specifically has to be in RSI for printf to work, at least...

I tried writing a functionally equivalent program in C to look at its compiled assembly, but it's pretty incomprehensible, with lots of seemingly unnecessary jumping around etc.

I think my main problem is that I often don't know which register I should use to store a given value, or whether I should even put it on the stack.

Thanks!

\$\endgroup\$
2
  • \$\begingroup\$ I understand that compilers output lines like "xor %rax, %rax" when they want zero. But for humans, don't we have like a "clear rax" mnemonic, a macro? // Anyway, thank you for the # comments, they're lovely, as are the symbolic labels. And please do offer a tag or Review Context that explains the particular processors you intend this code should run on. If you viewed the corresponding C code through the lens of godbolt.org, it wouldn't hurt to mention a link in your Question. \$\endgroup\$
    – J_H
    Commented Mar 8 at 17:51
  • \$\begingroup\$ Thanks for the comments @J_H I see your point about xor %rax, %rax potentially not being as clear as it could be, however from what I've seen it seems pretty common in handwritten assembly - perhaps a macro would be nice though! And yes, I'm surprised I didn't mention the processor this will run on; it's for x86-64, btw. For viewing the corresponding C code's assembly, I actually used gcc -S, so it wasn't so easy to link unfortunately. Thanks! \$\endgroup\$ Commented Mar 8 at 18:32

1 Answer 1

2
\$\begingroup\$
mov    $0, %eax
xor    %rax, %rax

The efficient way to zero a 64-bit register is to xor the register to itself, and because any writing to a 32-bit register will automatically zero the upper dword, it is best to xor the 32-bit version of the register to itself: xor %eax, %eax.
Using xor %rax, %rax requires a REX prefix byte while xor %eax, %eax does not. Shorter code is generally better.
The mov $0, %eax instruction has an even longer encoding and you should only use it in cases where you don't want flags modified.

cmp    $0, %rsi
jz end

The efficient way to find out if a 64-bit register is 0, is to test the register to itself. Here you would write test %rsi, %rsi followed by jz end. Following a cmp (like you were doing), it would have been more idiomatic to write je end. Both jz and je have the same encoding but je better conveys the meaning of two items being 'equal' to each other.

pushq  %rsi
call   printf
popq   %rsi
...
pushq  %rsi
call   puts
popq   %rsi

Before/after calls to printf and puts I need to preserve the value of RSI, since I use it as the argument counter. To do this I push/pop it from the stack, but perhaps it would be better to store it in some callee-saved register instead? But it still specifically has to be in RSI for printf to work, at least...

The %rsi register is a so-called call-clobbered register, see this Stack Overflow answer. So these printf and puts functions are allowed to use %rsi for their own purposes and leave it changed. That's why you had to preserve the register yourself. You can easily escape from this simply by choosing a call-preserved register instead. I would suggest you pick %rbx for your loop control variable.

loop:
  cmp    $0, %rsi # If we've printed everything, then finish
  jz end
  ...
  dec    %rsi
  jmp    loop
end:

An efficient loop tries to minimize the number of control transferring instructions (here jz and jmp), and prefers to put the loop condition's logic at the bottom of the loop. Remember that the dec instruction already defines a number of flags, including the ZF (zero flag), therefore you could replace the unconditional jmp loop by the conditional jnz loop. Of course, you would then also move the initial test-for-zero to outside of the loop.

All of the above in one code snippet:

main:
    mov    %rdi, %rbx    # argc
    mov    %rsi, %r12    # argv

    lea    message(%rip), %rdi
    mov    %rbx, %rsi
    xor    %eax, %eax
    call   printf

    test   %rbx, %rbx
    jz     end
next:
    mov    0(%r12), %rdi # Dereference r12 to get the current arg
    call   puts
    add    $8, %r12      # Advance r12 to point at the next arg
    dec    %rbx
    jnz    next
end:

    xor    %eax, %eax
    ret

Isn't argc always at least one? The initial test-for-zero seems not necessary then. And also, argc will be a small number, so we can write the shorter mov %edi, %ebx, mov %ebx, %esi, and dec %ebx instructions instead:

main:
    mov    %edi, %ebx    # argc
    mov    %rsi, %r12    # argv

    lea    message(%rip), %rdi
    mov    %ebx, %esi
    xor    %eax, %eax
    call   printf

next:
    mov    0(%r12), %rdi # Dereference r12 to get the current arg
    call   puts
    add    $8, %r12      # Advance r12 to point at the next arg
    dec    %ebx
    jnz    next

    xor    %eax, %eax
    ret
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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