6
\$\begingroup\$

I'm trying to work up to being able to print the hailstone sequence in assembly. To do that though, I first needed to learn how to actually print a number out using the write system call.

After a few design iterations, I ended up using division to get each digit one at a time by using the remainder. Each remainder gets pushed onto the stack, then the address of the stack is given to the system call to print out.

What I'd like advice on:

  • In a couple places, I have math split out over multiple lines, like:

    sub edx, esp ; Calculate how many were pushed
    dec edx
    

    It doesn't seem like it's possible to combine that into something like sub edx, esp - 0, but if there is a neater way, I'd like to know.

  • This is my first time using div. Is there anything wrong with how I'm using it? Also, is there a sane way of not using div altogether? Apparently it's stupid slow and should be avoided if possible.

  • Anything else notable. I'm a super-beginner, and this code is quite verbose.

I'm using n to represent the number that I want to print out. In theory though, that number could come from anywhere. It's just a placeholder for the exercise.

global _start
section .data
 n: dd 123456
section .text
 _start:
 mov ebp, esp ; So we can tell how many were pushed
 mov ecx, [n]
 .loop:
 mov edx, 0 ; Zeroing out edx for div
 mov eax, ecx ; Num to be divided
 mov ebx, 10 ; Divide by 10
 div ebx
 mov ecx, eax ; Quotient
 add edx, '0'
 push edx ; Remainder
 cmp ecx, 0
 jne .loop
 mov eax, 4 ; Write system call
 mov ebx, 1 ; STDOUT
 mov ecx, esp ; The string on the stack
 mov edx, ebp
 sub edx, esp ; Calculate how many were pushed
 dec edx
 int 0x80
 mov eax, 1
 mov ebx, 0
 int 0x80

Assembled and linked using:

nasm numprint2.asm -g -f elf32 -Wall -o numprint2.o 
ld numprint2.o -m elf_i386 -o numprint2
Sep Roland
4,78317 silver badges28 bronze badges
asked Jul 3, 2020 at 18:30
\$\endgroup\$
3
  • \$\begingroup\$ Just curious, how come you went with 32 bit over 64 bit? \$\endgroup\$ Commented Oct 5, 2020 at 0:43
  • 1
    \$\begingroup\$ @JoseFernandoLopezFernandez Our school decided to go with 32-bit. I believe it had to do with tools we used later (like immunity) being easier to use. We did all the debugging later on a 32-bit VM. \$\endgroup\$ Commented Oct 5, 2020 at 0:48
  • \$\begingroup\$ Consider implementing the division by reciprocal multiplication if you want to increase speed and/or reduce energy requirement. \$\endgroup\$ Commented May 18, 2023 at 12:25

1 Answer 1

2
\$\begingroup\$

print a number out using the write system call.
... using division to get each digit one at a time by using the remainder. Each remainder gets pushed onto the stack, then the address of the stack is given to the system call to print out.

Next is what the stack looks like in your program where you convert the number 123456. Every push will have written a dword on the stack:

'1',0,0,0,'2',0,0,0,'3',0,0,0,'4',0,0,0,'5',0,0,0,'6',0,0,0
^ ^ 
ESP EBP

Therefore, the code:

mov edx, ebp
sub edx, esp ; Calculate how many were pushed
dec edx

will make EDX equal to 23. However, the write system call expects from you the number of characters (bytes) in the string which should be 6.

... using div. Is there anything wrong with how I'm using it? Also, is there a sane way of not using div altogether? Apparently it's stupid slow and should be avoided if possible.

You are using it correctly. On modern hardware division is no longer the extremely slow operation that it used to be (same goes for multiplication). I would not seek to avoid it for this humble number to text conversion.

.loop:
 mov edx, 0 ; Zeroing out edx for div
 mov eax, ecx ; Num to be divided
 mov ebx, 10 ; Divide by 10
 div ebx
 mov ecx, eax ; Quotient
 add edx, '0'
 push edx ; Remainder
 cmp ecx, 0
 jne .loop

In the division loop, you can improve a lot:

  • zero a register using the xor reg, reg instruction
  • hoist initializing the loop-invariant const 10 out of the loop
  • don't copy back and forth between EAX and ECX
  • check a register for being zero through test reg, reg

Suggestion for a re-write

 mov ecx, esp
 sub esp, 16 ; Room for at most 10 digits (keep ESP dword-aligned)
 mov eax, [n]
 mov ebx, 10 ; CONST
.loop:
 xor edx, edx
 div ebx ; Divide EDX:EAX by 10
 dec ecx
 add edx, '0' ; From remainder [0,9] to character ['0','9']
 mov [ecx], dl
 test eax, eax ; Is quotient zero?
 jnz .loop
 mov eax, 4 ; Write system call
 mov ebx, 1 ; STDOUT
 lea edx, [esp+16]
 sub edx, ecx
 int 0x80
 add esp, 16 ; Clean-up

This is how the above version of the code uses the stack:

0,0,0,0,0,0,0,0,0,0,'1','2','3','4','5','6'
^ ^ ^ 
ESP ECX (after) ECX (before)
 <------- EDX = 6 ------>

Anything else notable.

Everyone is allowed their own programming style of course, but having the mnemonics, operands, and especially the tail comments aligned (like in my re-write) will certainly add to the readability of the code...

answered May 17, 2023 at 16:48
\$\endgroup\$
1
  • \$\begingroup\$ Even though modern hardware is better at division than older processors, decimal printing is a very common operation, so it might be worth reducing power draw by replacing it with a multiplication. That's arguably no longer a beginner implementation, though! \$\endgroup\$ Commented May 18, 2023 at 12:23

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.