14
\$\begingroup\$

I am fairly new to Assembly language programming and, for practice, I gave myself a problem: count down from 10 and right after 1, say "Blast off!".

section .text
    global _start
_start:
    mov eax, '10' ; asci 10
    mov [num], eax ; num now equals ascii 10

    mov edx, 2
    mov ecx, num
    mov ebx, 1
    mov eax, 4
    int 80h ; prints out '10' in the console

    mov eax, 9 ; numerical 9
    call subtract
subtract:
    call log ; logs the current number

    sub eax, 1 ; subract 1

    cmp eax, 0
    jne subtract ; if it doesn't equal 0, jump to subtract
    je blast_off ; if it does equal 0, jump to blast off
log:
    call to_ascii ; converts eax's value to ascii
    mov [num], eax ; num = eax
    mov edx, 1
    mov ecx, num
    mov ebx, 1
    mov eax, 4
    int 80h ; output the current number
    call to_number ; converts eax back to numerical
    ret
to_number:
    mov eax, [num] ; is this necessary? Moves num back into eax
    sub eax, '0' ; converts to numerical
    ret
to_ascii:
    add eax, '0' ; converts to ascii
    ret
blast_off:
    mov edx, len
    mov ecx, msg
    mov ebx, 1
    mov eax, 4
    int 80h ; outputs "Blast off!"

    mov eax, 1
    int 80h ; exits program
section .data
    msg db "Blast off!", 0xA ; message
    len equ $-msg ; length of message
section .bss
    num resb 1 ; this is for outputting the current number

Output:

1098654321Blast off!

Assembled: nasm -f elf blast_off.s

Linked: ld -m elf_i386 -s -o blast_off blast_off.o

My main concerns are:

  1. My code seems functional - is this bad, or does it not really matter, being assembly language?
  2. Is there repetition? This was a very difficult challenge for me, and at times I lost my head - sometime during those times I might have typed something completely "off topic" in regards to the subject of the code
  3. Aside from the first concern, does the organization need re-organizing? As in, should I reorder the different sections(.test, .data, .bss)? Or, does it not make a difference?
\$\endgroup\$
1
  • 1
    \$\begingroup\$ This link provides lots of tips for improving performance in assembly. mark.masmcode.com \$\endgroup\$
    – mbomb007
    Commented Dec 31, 2014 at 19:34

4 Answers 4

13
\$\begingroup\$

Section sequence

That's rather a matter of taste. Programmers used to 68k usually use the sequence .text, .data, .bss, programmers used to Java usually use the sequence .data, .bss, .text. In the assemblers known to me it doesn't make any difference, so it's rather a matter of taste or corporate policy.

Actually, strictly speaking it's not the assembler but the linker who might have a word to say about the sequence, and the linkers that I know don't mind the sequence (even the Keil Linker for 8051, L51, which is really weird stuff).

Functional code

Breaking up bigger chunks of code into smaller parts very often makes sense because very often it increases maintainability.

Basically in Assembler you do a lot of trade-off between maintainability, speed and size.

Your functions are not universally reusable, for example your function to_ascii performs no sanity checks. In that case, it doesn't always make sense, especially when the function body is just on instruction.

However, as far as I'm aware, on a modern member of the 80x86 family, instructions like call and ret basically come free of cost, thanks to the pipeline and branch prediction, so maybe there is no loss of speed, just of space.

Redundancy

The sequence

mov ecx, num
mov ebx, 1
mov eax, 4
int 80h ; output the current number

is redundant. I don't think it's entirely bad. Compared to having a single-instruction function to_ascii it looks, however, inconsistent.

I'd extract this into something like a print_number function.

Exit status

(You fixed this meanwhile.)

Your exit code looks like this:

mov ebx, 1
mov eax, 4
int 80h ; outputs "Blast off!"

mov eax, 1
int 80h ; exits program

The contents of register ebx are still 1 when you call exit. This is probably wrong. I do not see any indication why you would want to indicate that your program exits unsuccessfully. You probably want to insert a mov ebx, 0 like this:

mov ebx, 1
mov eax, 4
int 80h ; outputs "Blast off!"

mov ebx, 0
mov eax, 1
int 80h ; exits program

Asymmetry in to_number vs. to_ascii

to_number accesses [num] itself, whereas in the case of to_ascii, [num] is accessed by the caller. This is asymmetric, a form of inconsistent. In bigger programs this might lead to confusion with respect to how APIs work.

Name log

Nowadays when you say log people think of generic logging along the lines of Logging APIs like log4j or sclog4c.

I'd rather call this function something else, in order to prevent confusion.

Formatting

I'm not sure whether this is a convention, but I'd do this:

  • blank line before every new function (i.e. after a ret that is not followed by code which could be reached by a conditional jmp before the ret).
  • Use local labels for loop and branch labels inside a function.
  • Write a small function header on top of each function which describes input, output and access to globals (especially side-effects).

Function header

Here's a sample function header for to_ascii

; Returns the ASCII value of the input digit.
; @param eax digit (value in the range of [0..9]) which is converted into an ASCII digit.
; @return eax ASCII value (in the range of ['0'..'9']).
; @warning If eax is out of the supported range, the behavior is undefined.
to_ascii:
    add eax, '0'
    ret

Local labels vs Global labels

AFAIK this is also true for NASM:

  • Local labels start with '.' and are valid until the next global label.
  • Global labels do not start with '.'.

Looking at your code, subtract should be a local label and thus renamed to .subtract.

This is not mandatory, and there is a valid interpretation of your code which includes tail recursion and last call optimization in which your code and label structure are valid.

It's just a bit surprising (maybe even positively) to see such constructs in assembler.

Conditional jumps

At the end of subtract you have two conditional jumps:

jne subtract
je blast_off

While this code is correct, I think the following would be more clear:

je blast_off
jmp subtract

Or in case you want to optimize for speed

jne subtract
jmp blast_off

The point is maintainability. I found it less error-prone to have the last conditional jmp, which semantically is identical to an unconditional jmp as unconditional jmp indeed. When you change the conditions, there is less necessity of changing the last jmp, and therefore less risk of getting code which would just go haywire for running unintentionally into the next function, in case the current function doesn't end with ret itself.

Structured Programming regarding _start / subtract?

I personally would almost always follow structured programming even in Assembly language.

At _start / subtract, this is not done because you call subtract. Maybe that call even is unintentional, as I cannot see where subtract would ever return.

Independently of that, a construct like

    call foo
foo:
    more code

looks confusing, at least to me. First, foo is called, and everything that is called is expected to return, and then when foo returned, we would run it once more.

I guess in this case, best is to simply remove the call subtract statement.

Structured Programming regarding _start / blast_off?

A similar problem is there with _start and blast_off. blast_off is not called, so it is not a function - or to be seen as last-call optimization. I'd actually avoid putting the good-case exit anywhere outside start.

In general, I'd avoid jumping over functions. I feel jumps should be more local than calls, not the other way round. The reason is that jumps are where we program conditions and loops, which is transfer of control inside a function, whereas calls is where we call subroutines, which is the transfer of control to another function. I find it confusing if the transfer of control inside a function is "bigger" in distance than the transfer of control to another function.

Output

The output looks like this:

10987654321Blast off!

You might want to put space in between.

Makefile

You might want to use a Makefile for this.

I wrote the following Makefile for building and running this myself:

AS:=nasm
ASFLAGS:=-f elf 
LINK.o:=ld
LOADLIBES:=-m elf_i386 -s

.PHONY: all 
all: blast_off

blast_off: blast_off.o

.PHONY: run
run: blast_off
        ./$^

Disclaimer

I am new to NASM / x86 / x64 assembler myself. I'm very experienced in 680x0, ARM and somewhat experienced in 6502, Z80, 8051, 80251 and CalmRISC but, as said, not (yet) in x86 / x64.

\$\endgroup\$
5
  • \$\begingroup\$ Regarding the section, Structured Programming regarding _start / subtract?, I have a question: I often see the example like you wrote it: without a call. How does the assembler know when to jump to a label when it isn't told to them by call or jmp? \$\endgroup\$
    – SirPython
    Commented Dec 31, 2014 at 18:42
  • \$\begingroup\$ @SirPython The assembler then doesn't jump to that label. The label is just something which the Assembler uses to "remember" an address (relatively, the absolute address is inserted by the linker). You can insert labels anywhere in the code where you like, it won't change the sequence of execution in any way. A label doesn't start something new in the mind of the assembler, it only starts something new in the mind of the programmer. Which means that if you have the situation of _start wanting to execute the very next function / label subtract, no jmp, no call required. \$\endgroup\$ Commented Dec 31, 2014 at 18:44
  • \$\begingroup\$ @ChrisianHujer So, if I didn't have any calls, jumps, or recursion in my code, the assembler would just execute everything "down to the bottom"? \$\endgroup\$
    – SirPython
    Commented Dec 31, 2014 at 18:47
  • 1
    \$\begingroup\$ @SirPython Yes that's right. \$\endgroup\$ Commented Dec 31, 2014 at 18:53
  • \$\begingroup\$ Please note that having a function "run in" the next one is an anti-pattern. Someone seeing a listing might think it's possible to rearrange the functions into a more logical sequence. A "run in" would then suddenly execute something else. \$\endgroup\$ Commented Dec 31, 2014 at 22:03
5
\$\begingroup\$

As an assembly programmer, you are probably interested in reducing the size of your code.

The following instruction replacements result in a smaller file:

  • sub eax, 1 -> dec eax
  • mov ebx, 0 -> xor eax, eax
  • cmp eax, 0 -> test eax, eax
\$\endgroup\$
6
  • \$\begingroup\$ I've actually been meaning to ask this: why is xor eax, eax preferred over mov eax, 0? \$\endgroup\$
    – SirPython
    Commented Dec 31, 2014 at 19:28
  • 1
    \$\begingroup\$ @gyc One of the optimizations you suggested is not a good one. See mark.masmcode.com . Add/sub are both faster than inc/dec. \$\endgroup\$
    – mbomb007
    Commented Dec 31, 2014 at 19:33
  • \$\begingroup\$ @mbomb007 that's why I said "reducing the size of your code" :) (think about payload size for buffer overflows) But that test was conducted in 2004 on a P4, I wouldn't take it for granted. \$\endgroup\$
    – gyc
    Commented Dec 31, 2014 at 20:26
  • 1
    \$\begingroup\$ @SirPython: Using xor to clear a register resulted in a smaller instruction. It only requires a single byte to represent in machine code. Explicitly moving a value of 0 in on the other hand results in a larger instruction (I want to say 3 or 4 bytes). Nowadays it's no longer an issue but critical when you wanted to leave a small memory footprint back then when we had only a few MiB or KiB to work with. These days, it is still a useful instruction to use (I would even say, idiomatic). \$\endgroup\$ Commented Dec 31, 2014 at 20:40
  • 1
    \$\begingroup\$ @SirPython the code generated by xor eax,eax is "33 C0", for mov eax,0 it is "B800000000" \$\endgroup\$
    – gyc
    Commented Dec 31, 2014 at 20:46
5
\$\begingroup\$
  • Although the starting number of 10 may be obvious, you can still define a constant in case you want to use a different one:

    START_NUM   EQU   '10'
    
  • As you may already know, it's important to have plenty of comments in assembly code.

    But, there can still be obvious ones:

    sub eax, 1 ; subract 1
    

    In any context, it's clear that this subtracts 1 from EAX. You may, however, comment on why this is needed, in case it's not obvious.

    (and be sure to fix typos in comments)

  • The output doesn't look too nice with everything close together. It would be better to have a line break after each count, but I don't know the best code for that. If you cannot figure something out, then you can just add a space between each count, with everything still on the same line.

\$\endgroup\$
5
  • \$\begingroup\$ When you say "I'm not too familiar with interrupts", I get a little confused. I've seen you program some things in assembly before - is there something I'm missing with my interrupts?(int 80h is an interrupt, if I'm correct). Also, I notice in your code that you wrote: subtract 1. Is subtract a label or was is %defined? Because, if it's a label, I don't understand why you have the 1 after it. \$\endgroup\$
    – SirPython
    Commented Dec 31, 2014 at 21:30
  • \$\begingroup\$ @SirPython: My other programs on this site don't use interrupts, and the first time I've used them (in class) was for a 16-bit program. I may be completely wrong about that part. The subtract 1 in my answer was quoted from your code and is a comment. \$\endgroup\$
    – Jamal
    Commented Dec 31, 2014 at 21:34
  • \$\begingroup\$ @SirPython: Okay, it turns out that I was wrong about that, so I'll remove it. \$\endgroup\$
    – Jamal
    Commented Dec 31, 2014 at 21:37
  • \$\begingroup\$ Silly me! I didn't notice the semicolon. One more thing(I'm sorry if this is breaking the rules of comments): How is it possible to not use interrupts? How else are you to execute certain parts of your code? \$\endgroup\$
    – SirPython
    Commented Dec 31, 2014 at 21:42
  • \$\begingroup\$ @SirPython: Unfortunately, that is off-topic here (at least as a question or extended discussion). But, you could still ask some people in chat. \$\endgroup\$
    – Jamal
    Commented Dec 31, 2014 at 21:51
3
\$\begingroup\$
to_number:
mov eax, [num] ; is this necessary? Moves num back into eax
sub eax, '0' ; converts to numerical
ret  

The first comment here suggests you are only interested in getting num back into EAX. Then a slightly more elegant solution would be to not call to_number but push/pop EAX on the higher level. Like so :

log:
push eax
call to_ascii ; converts eax to ascii
mov [num], eax ; num = eax
mov edx, 1
mov ecx, num
mov ebx, 1
mov eax, 4
int 80h ; output the current number
pop eax
ret

This might get you into trouble when you will want more items in the BSS section.

mov [num], eax ; num = eax

You are writing the dword value from EAX at a location that was setup as a single byte! Either use num resd 1 or num resb 4

section .bss
num resb 1 ; this is for outputting the current number

At a number of places in your program you process a byte from the accumulator register. You consistently use EAX but it would be more to the point (and read a lot easier) if you used the AL register instead.

\$\endgroup\$
2
  • \$\begingroup\$ So are you saying that the numbers, when stored in their ASCII values, are actually DWORDS and not BYTES? \$\endgroup\$
    – SirPython
    Commented Jan 11, 2015 at 20:16
  • \$\begingroup\$ When you wrote mov eax,'10' you effectively put 00003031h in EAX. You can move up to 4 characters at once in any dword register. A character still remains a byte. \$\endgroup\$
    – Sep Roland
    Commented Jan 11, 2015 at 20:37

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.