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 call
ed, and everything that is call
ed is expected to ret
urn, and then when foo
ret
urned, 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 call
ed, 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.