4
\$\begingroup\$

I started learning assembly some days ago and I wrote my first program today. Nothing exceptional here: some user inputs, strings manipulations, integer to string conversion etc... The only purpose was to test some things. I would like to have your reviews and advices to improve my code. Also if i didn't make big mistakes.

I'm looking for advice about instructions alignment in loops for example. Except inserting nops, I don't have other idea. Or know the size of all instructions and predict the good alignment on 16 or 32 bytes boundaries.

SYS_READ equ 3
SYS_WRITE equ 4
SYS_EXIT equ 1
STDIN equ 0
STDOUT equ 1
%macro printm 2
 mov eax, SYS_WRITE
 mov ebx, STDOUT
 mov ecx, %1
 mov edx, %2
 int 0x80
%endmacro
%macro readm 2
 mov eax, SYS_READ
 mov ebx, STDIN
 mov ecx, %1
 mov edx, %2
 int 0x80
%endmacro
%macro prolog 0
 push ebp,
 mov ebp, esp
%endmacro
%macro epilog 0 
 mov esp, ebp
 pop ebp
%endmacro
%use smartalign
section .text
global _start
_start:
 push ebp
 mov ebp, esp
 and esp, 0xFFFFFFF0
 
 ; first check if our strlen proc works
 push dword msgbegin
 call strlen
 add esp, byte 4
 cmp eax, lenbegin
 jne .exit ; it works, we continue
 
 ; after we copy the msgbegin in string strdst
 mov ecx, lenbegin
 mov esi, msgbegin
 mov edi, strdst
 rep movsb
 ; we print the string to check if the memcpy is good 
 printm strdst, lenbegin
 ; after we ask for user to enter two number (1 digit each)
 printm msgbinp1, leninp1
 readm num1, 2
 printm msgbinp2, leninp2
 readm num2, 2
 ; user input to enter a number greater than one digit
 printm msgbinp3, leninp3
 readm bignum, 4
 ; we convert this string number to an (dword) integer value
 mov edx, bignum
 call atoi
 ; we compare it with 123 to check if atoi worked
 cmp eax, dword 123
 jne .exit ; exit if bignum != 123
 ; need to strip line feed from bignum
 printm bignum, 4
 printm msgoutp, lenoutp
 ; now we compute the sum with the first two digits
 mov al, byte [num1]
 sub al, '0'
 mov bl, byte [num2]
 sub bl, '0'
 add al, bl
 add al, '0'
 mov [sum], al
 
 ; we put the string msgres to uppercase
 mov esi, msgres
.next_char:
 lodsb
 test al, al ; check for end of string
 jz .done 
 cmp al, 'a' ; ignore unless in range
 jl .next_char
 cmp al, 'z'
 jg .next_char
 sub al, 0x20 ; convert to upper case 
 mov [esi-1], al ; write back to string
 jmp .next_char
.done:
 printm msgres, lenres
 ; we print the sum
 printm sum, 1
.exit: 
 ; exiting the programm
 mov eax, SYS_EXIT
 int 0x80
strlen:
 push edi
 mov edi, [esp + 8]
 sub ecx, ecx
 sub al, al
 mov ecx, -1
 cld
 repne scasb
 not ecx
 mov eax, ecx ; keep null term in size
 pop edi
 ret
atoi:
 xor eax, eax ; zero a "result so far"
.top:
 movzx ecx, byte [edx] ; get a character
 inc edx ; ready for next one
 cmp ecx, '0' ; valid?
 jb .done
 cmp ecx, '9'
 ja .done
 sub ecx, '0' ; "convert" character to number
 imul eax, 10 ; multiply "result so far" by ten
 add eax, ecx ; add in current digit
 jmp .top ; until done
.done:
 ret
 ; not implemented yet
rand:
 push ebp
 mov ebp, esp
 rdtsc
 xor edx, edx ; to always fit
 div dword [ebp + 8] ; range
 mov eax, edx
 mov esp, ebp
 pop ebp
 ret 
section .data
 msgbegin db "hello everyone !", 0xa, 0
 lenbegin equ $ - msgbegin
 msgbinp1 db "Enter a digit : ", 0xa, 0
 leninp1 equ $ - msgbinp1
 msgbinp2 db "Enter second digit : ", 0xa, 0
 leninp2 equ $ - msgbinp2
 msgbinp3 db "Enter third digit : ", 0xa, 0
 leninp3 equ $ - msgbinp3
 msgoutp db "is equal to 123 !", 0xa, 0
 lenoutp equ $ - msgoutp
 msgres db "sum of x and y is ", 0xa, 0
 lenres equ $ - msgres
 strdst times lenbegin db 0
segment .bss
 sum resb 1
 num1 resb 2
 num2 resb 2
 bignum resd 1
200_success
146k22 gold badges190 silver badges479 bronze badges
asked Jun 23, 2022 at 19:47
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Welcome to Code Review! I see that it prints messages like "hello everyone !", "Enter a digit :" and "Enter second digit :" but It would benefit reviewers to have a bit more information about what the code achieves in the description. From the help center page How to ask: "You will get more insightful reviews if you not only provide your code, but also give an explanation of what it does. The more detail, the better." \$\endgroup\$ Commented Jun 23, 2022 at 21:15
  • \$\begingroup\$ I edited my code with more comments. It implements strlen, print, user input, a toupper sequence. I hope it would help. I would like to have reviews about : mistakes and major errors and code improvisation. For example, in my atoi function, I have to use imul for the conversion. I know that imul is really time consuming. Maybe it would be faster with bitshift instructions. \$\endgroup\$ Commented Jun 23, 2022 at 21:30
  • 1
    \$\begingroup\$ re: zeroing AL for scasb: What is the best way to set a register to zero in x86 assembly: xor, mov or and? - use xor eax,eax. Also, repne scasb is fairly low-performance; SSE2 can go much faster even for small strings, especially if your buffer is known to be aligned. (Only rep movs / rep stos have "fast strings" microcode) re: atoi: some more optimization is possible with LEA, and to detect ASCII '0'..'9' inputs more efficiently as part of converting to integer 0..9: NASM Assembly convert input to integer? \$\endgroup\$ Commented Jul 1, 2022 at 1:07
  • \$\begingroup\$ Code alignment often isn't a huge deal these days, with CPUs running from a uop cache. The JCC erratum performance pothole for Skylake CPUs is a bigger deal for code alignment: How can I mitigate the impact of the Intel jcc erratum on gcc?. Usually just assemble and look at the output to see if there's anything problematic, and/or use perf stat -all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,idq.mite_uops or similar to look for high counts of legacy decode (MITE uops) \$\endgroup\$ Commented Jul 1, 2022 at 1:09

1 Answer 1

2
\$\begingroup\$

I think you can do better with aligning the text. You got comments that start all over the place and instead of separating the instruction from its operands with a single space character, you could adopt the more 'assembly way of doing things' and align the operands (and anything else) tabularly. See my snippets below.

The mention %use smartalign only enables the feature of the multibyte nop's. You still have to use the normal align 16 macro to get the desired padding.

You have included a cld instruction in the strlen subroutine. Normally you can trust the direction flag to be clear (unless of course you yourself toggled it for some reason) and only issue a single cld when your program begins.

Although the byte tag in add esp, byte 4 is fine in order to avoid the longer dword form that NASM would generate without optimizations, I see no reason to write cmp eax, dword 123 as the dword form would be the default here.
I find NASM is confusing in how it treats these immediate operands, so do yourself a favor and don't write these size tags but always assemble with the -O2 option.

For your strlen you used a parameter on the stack and for your atoi you used a parameter in a register. Both methods are fine and especially in the context of exploring the assembly language. However for a more serious program I would be inclined to say that the author should make their mind up.

sum resb 1
num1 resb 2
num2 resb 2
bignum resd 1

Data alignment is important. Since the .bss section is dword-aligned by default, you should have put the dword-sized bignum on top. At least that was what I first read. As it turns out, bignum is not meant to be a dword at all! It is a string that happens to hold 4 bytes. You should write this as bignum resb 4. (It can stay put).

cmp eax, lenbegin
jne .exit
mov ecx, lenbegin

This is a missed opportunity to write the shortest code. If the branch wasn't taken you know that EAX is equal to lenbegin. Then best setup ECX from EAX (mov ecx, eax). It will shave off 3 bytes.

About strlen

The preferred way to zero a register is through xoring the register with itself.
If you are going to load the ECX register whole with mov ecx, -1, then it's useless to also zero ECX beforehand.
Because you chose to use repne scasb and apparently want to follow some register convention, you had to push EDI and allow ECX to get clobbered.
Below is my version of this code using a simple loop and nothing more than the EAX register:

; IN (stack) OUT (eax)
strlen:
 mov eax, [esp+4] ; Address of string
.a: cmp byte [eax], 0
 lea eax, [eax+1] ; EAX++ without disturbing flags
 jne .a
 sub eax, [esp+4] ; Length is 'zero-terminator-included'
 ret

About atoi

You can condense the part that checks the validity of the character and that converts the character to number, to just 3 instructions: sub ecx, '0' cmp cl, 9 ja .done.
You can speedup the part that multiplies the current result and that adds the newest digit with a couple of lea instructions: lea eax, [eax + eax * 4] lea eax, [ecx + eax * 2].
You can avoid the unconditional branch jmp .top simply by re-arranging the code. Having jumps that execute repeatedly is costly.

; IN (edx) OUT (eax) MOD (ecx,edx)
atoi:
 xor eax, eax ; Result = 0
 jmp .b
.a: lea eax, [eax + eax * 4] ; Result * 10 + Digit
 lea eax, [ecx + eax * 2]
.b: movzx ecx, byte [edx] ; Fetch character
 inc edx
 sub ecx, '0' ; Convert ["0","9"] to [0,9]
 cmp cl, 10
 jb .a
 ret

About ucase

You have inlined the uppercase conversion. This actively harms the readability of your program. And anyway, it's a conversion that deserves its own subroutine.
The ASCII codes are unsigned quantities. It's best to use the unsigned conditional branches on them, so jb and ja.
Just like was the case with atoi, you can avoid the unconditional branch jmp .next_char by re-arranging the code.

; IN (esi) OUT () MOD (eax)
ucase:
 push esi
 lodsb
 test al, al ; Check end-of-string
 jz .c 
.a: cmp al, 'a' ; Ignore if not in ["a","z"]
 jb .b
 cmp al, 'z'
 ja .b
 sub al, 32 ; Convert to upper case 
 mov [esi-1], al ; Update string
.b: lodsb
 test al, al ; Check end-of-string
 jnz .a
.c: pop esi
 ret

About code alignment

Consider the example of the strlen subroutine. It's not really the strlen label itself that you would want to align but rather the .a label inside because it gets repeatedly jumped to.

Method 1 - on the execution path

; IN (stack) OUT (eax)
strlen:
 mov eax, [esp+4] ; Address of string
 ALIGN 16 ; (*)
.a: cmp byte [eax], 0
 lea eax, [eax+1] ; EAX++ without disturbing flags
 jne .a
 sub eax, [esp+4] ; Length is 'zero-terminator-included'
 ret

(*) Because 'smartalign' was enabled, NASM will pad with some multibyte nop instruction. This nop gets executed, and under circumstances it could even be another jmp. Read about this in the NASM manual chapter 5 paragraph 2.

Method 2 - outside the execution path

 times (15-($-$$+4+15) % 16) db 0 ; (**)
; IN (stack) OUT (eax)
strlen:
 mov eax, [esp+4] ; Address of string
.a: cmp byte [eax], 0
 lea eax, [eax+1] ; EAX++ without disturbing flags
 jne .a
 sub eax, [esp+4] ; Length is 'zero-terminator-included'
 ret

(**) This times prefix inserts just the right amount of padding bytes to have the local .a label paragraph-aligned. The number 4 in the expression is the length of the instruction mov eax, [esp+4]. In this case there's nothing additional that gets executed.

answered Jul 1, 2022 at 18:21
\$\endgroup\$
1
  • \$\begingroup\$ only issue a single cld when your program begins. - the i386 System V ABI guarantees that cld is clear on function call/return (thus at the top of main), and in the initial process environment (thus at the top of _start). So you never need cld in a Linux program unless your own code used std somewhere. \$\endgroup\$ Commented Apr 20, 2023 at 10:44

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.