I'm trying to code a simple person database using structs and arrays in assembly.
Each person has a firstName
, lastName
, age
and weight
. The goal is to have an infinite loop that asks the user whether they want to read or write to the array then ask them for a position to read or write to. Currently, the array is limited to a max of 5, plan is to increase that (as well as move the request for the position out both the .writing
and .reading
blocks).
A couple of questions:
- Formatting: Is the overall structure of the assembly okay? Any improvements that could be made to the formatting and structure?
- Accessing the elements of the array: For simple integer arrays, you can access them with
[rax + rcx * (size of int)]
whererax
is the pointer to the start of the array andrcx
is the position in the array we want to access. That didn't work for the Person array; I couldn't use[rax + rcx * Person.size]
. So is there a better way to do that access? - Other comments in general?
Compiled as follows using VS2017 x64 Native Tools Command Prompt
:
> nasm -g -fwin64 person.asm > cl /Zi person.obj msvcrt.lib legacy_stdio_definitions.lib
person.asm
default rel
extern scanf
extern printf
STRUC Person
.first_name: resb 50
.last_name: resb 50
.age: resq 1
.weight: resq 1
.size:
ENDSTRUC
SECTION .data
number: dq 0
scanf_fmt_1: db " %s", 0
scanf_fmt_2: db " %lld", 0
scanf_fmt_3: db " %c", 0
printf_fmt_1: db "Enter a number (1-5): ", 0
printf_fmt_2: db "Enter a first_name: ", 0
printf_fmt_3: db "Enter a last_name: ", 0
printf_fmt_4: db "Enter an age: ", 0
printf_fmt_5: db "Enter a weight: ", 0
printf_fmt_6: db "[R]ead or [W]rite? ", 0
printf_fmt_7: db "Person (%s %s) is %d years old and weights %d", 10, 0
SECTION .bss
person_array: resb Person.size * 5
SECTION .text
global main
main:
sub rsp, 32
.continue_loop:
lea rax, [p] ; load address of p (or John Doe)
lea rcx, [printf_fmt_6] ; address of "[R]ead or [W]rite? "
call printf
lea rdx, [number] ; address of number variable
lea rcx, [scanf_fmt_3] ; address of " %c"
call scanf
cmp qword [number], 'W' ; compare read value against W
je .writing ; local writing label
jmp .reading ; local reading label
.writing:
lea rcx, [printf_fmt_1] ; address of "Enter a number (1-5): "
call printf
lea rdx, [number] ; address of number
lea rcx, [scanf_fmt_2] ; address of " %lld"
call scanf
lea rbx, [person_array] ; we can't use fancy address calculation so manual it is
mov rax, Person.size
imul qword [number]
sub rax, Person.size ; we force the user to start at 1, but we start counting at 0
add rbx, rax
lea rcx, [printf_fmt_2] ; address of "Enter a first_name: "
call printf
lea rdx, [rbx + Person.first_name] ; address of first name
lea rcx, [scanf_fmt_1] ; address of " %s"
call scanf
lea rcx, [printf_fmt_3] ; address of "Enter a last_name: "
call printf
lea rdx, [rbx + Person.last_name] ; address of last name
lea rcx, [scanf_fmt_1] ; address of " %s"
call scanf
lea rcx, [printf_fmt_4] ; address of "Enter an age: "
call printf
lea rdx, [rbx + Person.age] ; address of age
lea rcx, [scanf_fmt_2] ; address of " %lld"
call scanf
lea rcx, [printf_fmt_5] ; address of "Enter a weight: "
call printf
lea rdx, [rbx + Person.weight] ; address of weight
lea rcx, [scanf_fmt_2] ; address of " %lld"
call scanf
jmp .end
.reading:
lea rcx, [printf_fmt_1] ; address of "Enter a number (1-5): "
call printf
lea rdx, [number] ; address of number
lea rcx, [scanf_fmt_2] ; address of " %lld"
call scanf
lea rbx, [person_array] ; we can't use fancy address calculation so manual it is
mov rax, Person.size
imul qword [number]
sub rax, Person.size ; we force the user to start at 1, but we start counting at 0
add rbx, rax
push qword [rbx + Person.weight] ; value of weight
sub rsp, 32 ; personal shadow space because > 4 params
mov r9, [rbx + Person.age] ; value of age
lea r8, [rbx + Person.last_name] ; address of last name
lea rdx, [rbx + Person.first_name] ; address of first name
lea rcx, [printf_fmt_7] ; address of "Person (%s %s) is %d years old and weights %d"
call printf
.end:
jmp .continue_loop
ret
-
\$\begingroup\$ @Mr. Vix don't edit the code. Feel free to point out bugs in an answer. Red more in the answers to this Meta post: _Should you edit someone else's code in a question? \$\endgroup\$Sᴀᴍ Onᴇᴌᴀ– Sᴀᴍ Onᴇᴌᴀ ♦2018年10月10日 23:21:07 +00:00Commented Oct 10, 2018 at 23:21
-
\$\begingroup\$ I'm experiencing similar problems in accessing the elements of the array (Problem #2). I wonder if you were able to solve your issue? Thanks. \$\endgroup\$lostcoder– lostcoder2023年05月17日 12:01:59 +00:00Commented May 17, 2023 at 12:01
1 Answer 1
- Formatting: Is the overall structure of the assembly okay? Any improvements that could be made to the formatting and structure?
You have applied the nice tabular format to the source text. That's certainly fine! But I notice that both jmp .end
and jmp .continue_loop
are falling a bit short.
Also I would apply the same format to the structure:
STRUC Person
.first_name: resb 50
.last_name: resb 50
.age: resq 1
.weight: resq 1
.size:
ENDSTRUC
- Accessing the elements of the array: For simple integer arrays, you can access them with
[rax + rcx * (size of int)]
whererax
is the pointer to the start of the array andrcx
is the position in the array we want to access. That didn't work for the Person array; I couldn't use[rax + rcx * Person.size]
. So is there a better way to do that access?
Since the size of the Person structure is 108 bytes you couln't use that value for a scale value because the CPU limits the possibilities to the values 1, 2, 4, and 8.
For your manual address calculation you used imul qword [number]
with RAX equal to the Person.size. This instruction is performing the widening multiplication that leaves its result in RDX:RAX. There's no real need to clobber the RDX register through using this 128-bit operation that Intel advices against (for efficiency reasons)! Just use the non-widening form of imul
that has 3 operands: imul rax, [number], Person.size
.
To account for the user-inputted number being 1-based, you then subtract Person.size one time. That is correct but you are wasting an instruction since you can combine this operation already in the lea
that loads the address of the array: lea rbx, [person_array - Person.size]
. Alternatively, you could opt to work 0-based internally but still allow the user to think 1-based. See the very last snippet in this answer.
Finally, re-arranging the code leaves us with next solution:
imul rax, [number], Person.size
lea rbx, [rax + person_array - Person.size]
- Other comments in general?
i. Alignment can be important for efficient access to the data. For the Person structure I would place those qword members first, followed by the byte members, and then pad the whole to obtain a size that's divisible by 8:
STRUC Person
.age: resq 1
.weight: resq 1
.first_name: resb 50
.last_name: resb 50
.filler: resb 4
.size:
ENDSTRUC
ii. Unlike it was in the 8086 era, x86 has no problem with the (limited) range of the conditional jumps. Therefore a construct like:
je .writing
jmp .reading
.writing:
is better replaced by:
jne .reading
.writing:
iii. Currently the program duplicates a number of operations. You should place these in a subroutine like below:
; IN() OUT (rbx)
LocateRecord:
lea rcx, [printf_fmt_1] ; "Enter a number (1-5): "
call printf
lea rdx, [number]
lea rcx, [scanf_fmt_2] ; " %lld"
call scanf
imul rax, [number], Person.size
lea rbx, [rax + person_array - Person.size]
ret
In your defense, apparantly you were "planning to move the request for the position out both the .writing
and .reading
blocks".
Working 0-based internally might be preferrable:
; IN() OUT (rbx)
LocateRecord:
lea rcx, [printf_fmt_1] ; "Enter a number (1-5): "
call printf
lea rdx, [number]
lea rcx, [scanf_fmt_2] ; " %lld"
call scanf
dec qword [number] ; [1,5] -> [0,4]
imul rax, [number], Person.size
lea rbx, [rax + person_array]
ret