The x86-64 assembly syllabus is live

For sure this is a problem of experience. As we become more experienced with a subject, and start to internalize things, we start to more easily forget the areas in which we did not know, and how we came to know those things. Unless you are actively working with those that do not know but are learning, it is easy to forget.

So as, as mentioned by @SleeplessByte having a beginner (even if they already know “programming”) can be an invaluable source of what is missing, or what might be addressed.

3 Likes

@oxe-b I think you mostly mis a section on how the writing is supposed to work, which is indeed something I cannot guess :hugs:

1 Like

There is an observation at the end of the instructions for this task:

Notice that `combining_function` may modify the values in registers you are using.
Make sure to save any variable you need in memory before calling the function.

Not directly related to your error, but I realized (from your thought process) that I should’ve made it clear that it is not possible to operate memory to memory, like so mov [a], [b]. Memory usually can be destination operand or source operand, but not both at the same time.

I’ll also create an entry on writing to memory. It is done in the same way, but it seems I forgot to make this explicit and didn’t even provide an example…

1 Like

Right. So

; First store the address for later
mov r8, rdi

Should that have been

; First store the address for later
mov r8, qword [rdi]

?

(P.S. I am having loads of fun with this btw)

3 Likes

No, you need the address in order to write to it later, so you did right in saving rdi. The problem is that r8 may be modified by the called function. Registers are global and any one you can use can also be used by other functions. I’ve tried to hint at what should be done by saying you should save things in memory.

1 Like

I see my mistake. I was thinking: I can use any of the free registers as memory, but you mean: use a variable!

I will try that now. Thanks, this is great.

Edit

So. I was thinking:

  1. Add a new variable in .data
  2. Move the address into it
  3. Finally write the eax result to that address location, somehow.

I suppose this is the first step

mov qword [rel color_dest], rdi

Then I guess I can do

lea rdi, [rel color_dest]
mov [rdi], eax

The idea being: the address is stored in the relative address color_dest, so I need to get it out, then I can store eax at that address.

All returns are still 0, so I am missing something fundamental :stuck_out_tongue:

2 Likes

I think that right now all elements are in place, you only need to reread the instructions and specifically what you get as arguments and what each thing does.

1 Like

The hard part for me is that everything I try is segfaulting. Anyway, here is the journey.

Sooo I read the test: combine red with yellow. That means that I would expect the tests to:

  • add_base_color with RED or YELLOW
  • call make_color_combination with dest and the other one (RED or YELLOW)

So next I started debugging. Comment out call combining_function and instead write esi directly to my output address:

mov qword [rel color_dest], rdi 
mov esi, dword [rsi]

lea r8, [rel color_dest]
mov dword [r8], esi

That resulted in all 0s. Maybe I am not correctly writing to the location. I can test this by commenting out everything and directly writing to the incoming rdi

mov r8, [rel RED]     ; store red in r8
mov [rdi], r8         ; write r8 to address

No dice: segmentation fault. Maybe I need to only get the 32bit values:

mov r8d, [rel RED]     ; store red in r8
mov [rdi], r8d         ; write r8 to address
ret

No dice: segmentation fault. Maybe I can try to directly write a constant.

mov dword [rdi], 42

No dice: segmentation fault. Perhaps with a full 32-bit value?

mov dword [rdi], 0xFF000000

No dice: segmentation fault.

From the tests of add_base_color, I know that this is the correct syntax to write a value to a location in memory:

mov dword [rel base_color], eax

Soooo Yeah. I can re-read the instructions for task 4 a gazillion times, but something isn’t adding up here :smiley: What I am going to do now (do not tell me yet), is start over, and see what I missed!

brb!

EDIT

Okay, I seem to have removed an ret earlier, which made the segfaults happen sometimes. Now that this fixed, the following actually works:

; First store the address in memory for later
mov qword [rel color_dest], rdi 

; Combine color
mov edi, dword [rel base_color]  ; get current 32-bit base color
mov esi, dword [rsi]             ; get passed in 32-bit secondary color
call combining_function

 ; Extract result
mov r8, [rel color_dest]
mov dword [r8], eax
ret

…which is pretty much what I had before. I also realised I could have used get_color_value and shuffle rdi around but in this case, I don’t think that makes the code clearer.

Takeaway

I do not think this final part was a mistake with the exercise.

  • I think it would be good to list something about writing to memory (like you mentioned)
  • I think it would be good to list something about memory being either left or right side (like you mentioned)
  • It seems lea is a shortcut for something? But I don’t know where I would use it.
  • I think the hints could have a pointer to “you can use variables to store memory”
  • I think the hints could have a pointer about rel for PIPE error

Overal: I enjoyed this again. I think how dereferences works isn’t completely clear after finishing this exercise, but I do feel cool about being able to solve it.

(I’ll try more tomorrow!)

2 Likes

Right now it is only a way to get the address for a variable. It is more idiomatic to use lea dest, [var] than mov dest, var.

In the arrays concept, the full power of lea should be unleashed (or at least explained).

There is a hint for that in the General header in hints.md but for some reason no hint in this header appears in any of those concept exercises. I may need to copy them to each task’s section.


I’ll make a PR at some point later today or tomorrow morning with many changes based on what you’ve shared.

Once again, thank you very very much for your feedback!

2 Likes

oxe-b informed me via discord that this style of feedback is extremely helpful, so I will “learn in public” I suppose!

Arrays (Bird Watcher)

Before doing anything, I start my prework by adding empty function definitions and positional argument register comments, like I did for everything else. By now I remember that rdi is the first positional register as parameter, but forgot that that is dil for byte, so I had to open up Lasagna to check. I’ll get there!

I read that some have a 1 byte parameter (dil) and one has an 8 byte parameter (rdi) (because 8 * 8 = 64). I’ll now read the instructions.

On most platforms, this data is filled with zero by the OS at the start of the program.

Wondering if that means it’s good practice to zero-fill it before usage.

last_week_counts

Alrighto. I need to return an array with 8 bytes, with the last one zeroed. The initial value is 0, 2, 5, 3, 7, 8 and 4. I remember that Assembly is little-endian, meaning that the least significant byte comes first. So I think this is written in a way where I can make an array like so (pseudo):

[0 2 5 3 7 8 4 0]

I will do that using arr I suppose, as the example has that. I don’t understand why there is this:

section .data
   arr dq 4, 8, 15, 23, 42

…but also this:

section .data
   example dd 4, 5, 6, 18, 20, 76, -12, 34

and then I realise arr is the label of an array. Array initialization is nothing more than label size data1, data2, datan. Got it.

I forgot which size is which keyword, so I look it up in Color Palette:

keyword size
db 1 byte
dw 2 bytes
dd 4 bytes
dq 8 bytes

Each bird count is a byte, so db it is.

section .data
   last_bird_counts db 0, 2, 5, 3, 7, 8, 4, 0

Dunno what the point is of that last 0 because if I leave it off I have an array of length 7. Maybe it will become clear.

It returns a copy of last week’s counts as a 8-byte number

I need to copy this to rax. I wonder if I can “just” combine the bytes by giving back the start of the array. Given that rax will then start at that address, it will hold the next 8 bytes (rax is 64 bit). I don’t know if this means copy, but let’s try it.

 lea rcx, [rel last_bird_counts]
 mov rax, qword [rcx] 
 ret

The problem is that I cannot test this. The test passed before this code, and the test is still passing, so no clue if I did it right. I will move on and see if the next exercise will break my assumptions (tests are not passing for those)

:tada: I now think i know why I needed 8 elements. The last one being 0 means I can “just” convert it to a single 64-bit result.

current_week_counts

Sounds like the same exercise, except that I need to initialize it all to 0s and I need to return the second number which is “until what day we have counted”

section .data
   next_bird_counts db 0, 0, 0, 0, 0, 0, 0, 0
          day_index dq 0

Then I use the same trick to load the variables

 lea rcx, [rel next_bird_counts]
 mov rax, qword [rcx] 

 lea rcx, [rel day_index]
 mov rdx, qword [rcx]
 ret

It passes the tests, so sofar, so good.

save_count

I think I need to do the following:

  • if day_index is equal to 8
    • then 7 days have been counted
    • set last to be next (current)
    • set day_index to 0
    • clear next

Then I need to

  • Add incoming count at next[day_index]
  • Increase day_index by 1

I know that the array is made of bytes so I will need to multiply next_day_counted by the size of each element (8).

Let’s start with the bottom part

mov rdx, [rel day_index]         ; find current index
lea rcx, [rel next_bird_counts]  ; find address of first index

mov [rcx + 8*rdx], dil           ; store incoming byte at location
add [rel next_bird_counts], 1    ; add one to the count

I get an error about the size being unknown. I think that makes sense because it doesn’t know that that 1 is a byte.

add byte [rel next_bird_counts], 1

It now compiles, but the tests complain.

save_count_first_day
Expected 5 Was 6

Ah. I think I made the same mistake again. I need to add ret to each function. Let me try that.


Now my previous test fails

last_week_default
Expected 0 Was 227

That is interesting. Why would it expect 0? I don’t really know what it is testing, so I don’t really have a good way to debug what’s wrong. In order to progress, I decide to look up the test and see if that helps me.

And what is today_count() ?

That is a function I have to implement. I haven’t done that yet, because that is task 4! Will that doesn’t really work.

I guess I will do that now so I can get hopefully to a passing test.


today_count

Alright. I can use day_index and subtract 1 to get the last counted day. It tells me I do not have to worry about -1.

mov rdx, [rel day_index]         ; find current index
sub rdx, 1                       ; look at last counted day
lea rcx, [rel next_bird_counts]  ; find address of first index

mov al, byte [rcx + 8*rdx]       ; retrieve byte at location

Yay. Now my Task 2 test is passing again. Let’s get back to the task at hand.


save_count (cont.)

I’ll again look at test and see where this 5/6 may be coming from.

  • 5 may be expected as result of last_week_counts.
  • 5 may be expected as crt.counts.

Without looking at the implementation, assume now that line 37 checks that last_week_counts() has not been changed, and that the actual issue is that I am (somehow) returning 6 instead of 5.

Well yeah sherlock. Ya done something stupid:

  mov rdx, [rel day_index]         ; find current index
  lea rcx, [rel next_bird_counts]  ; find address of first index

  mov [rcx + 8*rdx], dil           ; store incoming byte at location
+ add [rel next_bird_counts], 1    ; add one to the count

That doesn’t add one to the count. That adds one to the index. Dumb dumb dumb. Fixing it.

This changes everything:

My guess is that that is a test that expects me that I implemented some of the code that I have not implemented yet, and it hits a loop or something like that. I will ignore this error and see if I can finish the exercise anyway…


Back to my save_count. I need to add the conditional to swap that thang. I think I will write it like this:

  • if day_index > 7, goto swap
  • else goto fill

Then swap can goto fill and everything stays the same logic.

    mov rdx, [rel day_index]         ; find current index
    cmp rdx, 7                       ; if day index >= 7
    jae save_count.swap              ; ..., then 7+ values are stored
    jmp save_count.store
    ret

Then I move my previous code to .store:

.store:
    mov rdx, [rel day_index]         ; find current index
    lea rcx, [rel next_bird_counts]  ; find address of first index

    mov [rcx + 8*rdx], dil           ; store incoming byte at location
    add byte [rel day_index], 1      ; add one to the count
    ret

And add .swap:

.swap:
    call current_week_counts               ; get current week count as single number
    mov qword [rel last_bird_counts], rax  ; store rax as last week count
    mov qword [rel next_bird_counts], 0x0  ; clear array
    mov byte [rel day_index], 0x0          ; clear day index
    jmp save_count.store
    ret

Now what I am uncertain of is if that mov actually clears the bytes (reset them).

5 tests now pass.

I have not yet implemented update_today_count, so that’s next.


update_today_count

This reads as: get count of last day, add one, store it.

  • call today_count
  • add parameter
  • store it
call today_count
add al, 1

mov rdx, [rel day_index]         ; find current index
sub rdx, 1                       ; look at last counted day
lea rcx, [rel next_bird_counts]  ; find address of first index

mov [rcx + 8*rdx], al            ; store updated count

Eh yeah, not add al, 1, but add al, dil.

Rejoice. But save_count is failing. I think what’s going wrong is the swap/store/reset.

Expected 1134726115295744 Was 0. Counts for last week are different than expected.

I think 1134726115295744 is the number you get when you take 0, 2, 5, 3, 7, 8, 4, 0 as a number?

Decimal 1134726115295744 to binary conversion into bytes:

Address 0 1 2 3 4 5 6
Data 00 02 05 03 07 08 04

Yes, okay. So if this is telling me it gets 0, I may be resetting it too early. Lets reset to the number 42 to test my theory

mov qword [rel last_bird_counts], 42  ; store rax as last week count

Expected 1134726115295744 Was 42. Counts for last week are different than expected.

I need to focus on that line. Maybe I can try to not call current_week_counts and store [rel next_bird_counts] instead.

mov r8, qword [rel next_bird_counts]   ; get current week count as single number
mov qword [rel last_bird_counts], r8   ; store r8 as last week count

That doesn’t change anything unfortunately.

cont: The x86-64 assembly syllabus is live - #41 by SleeplessByte

3 Likes

Thank you once again, @SleeplessByte !

I’ve made a large PR that addresses part of those problems, but not others. I’ll look into the remaining issues now. It might take a while until it is reviewed, I’ll let you know!

1 Like

Is my assumption about the tests somewhere near the truth? In other words: should I try to continue to see if the timeout goes away?

I also realise that I am causing an overflow because save_count is not yet complete. I’ll do that first!

2 Likes

The first test has an error because I was using “last_weeks_counts” instead of “last_week_count” in the generator :sweat_smile:

But I think the other tests are all right. Many depend on previous ones and things go back and forth, so you should do tests/functions/tasks in order.

It might be you are missing a ret in one of the functions? Can you paste your code here?

1 Like

I was, but that’s not the issue now. I will show you the code that is broken, and I also “fixed” that. I think it was me not implementing save_count completely and writing in “memory” where it shouldn’t.

I’ll show you in the message above how I wrote save_count. I am running into test-order issues again, but you’ll see (and you may already have fixed them)

; Bird Watcher (Arrays)

section .data
   last_bird_counts db 0, 2, 5, 3, 7, 8, 4, 0
   next_bird_counts db 0, 0, 0, 0, 0, 0, 0, 0
          day_index dq 0
   
section .text

global last_week_counts
last_week_counts:
    ; This function takes no parameter
    ; It returns a copy of last week's counts as a 8-byte number
    ; At the start of the program, last week's counts are 0, 2, 5, 3, 7, 8 and 4
    ; The last byte of the return value is always zero
    
    lea rcx, [rel last_bird_counts]
    mov rax, qword [rcx] 
    ret

global current_week_counts
current_week_counts:
    ; This function takes no parameter
    ; It returns two values:
    ; - A copy of current week's counts as a 8-byte number.
    ; - The number of days already filled in the current week, as a 8-byte number.
    ; All days after the most recent one should have its corresponding byte zeroed-out in the output
    ; At the start of the program, there is no count for the current week
    lea rcx, [rel next_bird_counts]
    mov rax, qword [rcx] 

    lea rcx, [rel day_index]
    mov rdx, qword [rcx]
    ret 

global save_count
save_count:
    ; This function takes as parameter the most recent count, as a 1-byte number ; di(l)
    ; It must save this value in a new entry for the current week
    ; If there is already 7 entries in the current week before the function is called, then:
    ; - The current week becomes the last week.
    ; - A new entry is added with the passed value in a new current week.
    ; The function has no return value

    mov rdx, [rel day_index]         ; store current index
    lea rcx, [rel next_bird_counts]  ; find address of first index

    mov [rcx + 8*rdx], dil           ; store incoming byte at location
    add byte [rel day_index], 1      ; add one to the count
    ret

global today_count
today_count:
    ; This function has no parameter
    ; It returns the most recent entry for the current week, as a 1-byte number
    mov rdx, [rel day_index]         ; store current index
    sub rdx, 1                       ; look at last counted day
    lea rcx, [rel next_bird_counts]  ; find address of first index

    mov al, byte [rcx + 8*rdx]       ; retrieve byte at location
    ret
    
global update_today_count
update_today_count:
    ; This function takes as parameter a 1-byte number ; di(l)
    ; It adds this number to the most recent entry for the current week
    ; This function has no return value
    ret

global update_week_counts
update_week_counts:
    ; This function takes as parameter a 8-byte number ; rdi
    ; Each byte in the input parameter, but the last, represents a day's count in the current week
    ; The last byte in the input parameter has no meaning and must be zeroed-out
    ; This function makes the following changes:
    ; - The current week becomes the last week.
    ; - The counts in the input parameter are fully inserted in the current week.
    ret

%ifidn __OUTPUT_FORMAT__,elf64
section .note.GNU-stack noalloc noexec nowrite progbits
%endif

Previous post updated, now working on update_today_count

2 Likes

I’ve merged now a PR that addresses lots of stuffs. Can you update and check if things are clearer?

1 Like

Yes. As soon as it pops off!

Edit: @oxe-b only tests updated? I see the new error messages. Much easier to understand.

1 Like

There were many changes in more than 40 files. If I didn’t miss any, all tests in all concept exercises now have messages like this one. I’ve also added tables to instructions like in lasagna and inventory management. And various changes in concept’s text.

I didn’t change the exercises’ instructions, though. So if anything is unclear, please let me know.

EDIT:

I’ll change the messages in those tests at some point later to show the expected result as a bitstring (instead of a decimal number). Thank you!

2 Likes

Oh exciting. I will go back into the exercises after I finish this one and inspect it!

Yeah I think maybe as little-endian bytes would help! Easier to read

Right now I don’t understand where I have fucked up:

mov rdx, [rel day_index]         ; find current index
cmp rdx, 7                       ; if day index >= 7
jae .swap

Doesn’t seem to matter if I use 7 or 8 or 6 or 12. I cannot get save_count to work, but qword [rel next_bird_counts] always returns 0 here (which I am then storing in last_bird_counts) or so it seems.

Total code thusfar:

; Bird Watcher (Arrays)

section .data
   last_bird_counts db 0, 2, 5, 3, 7, 8, 4, 0
   next_bird_counts db 0, 0, 0, 0, 0, 0, 0, 0
          day_index dq 0
   
section .text

global last_week_counts
last_week_counts:
    ; This function takes no parameter
    ; It returns a copy of last week's counts as a 8-byte number
    ; At the start of the program, last week's counts are 0, 2, 5, 3, 7, 8 and 4
    ; The last byte of the return value is always zero
    
    lea rcx, [rel last_bird_counts]
    mov rax, qword [rcx] 
    ret

global current_week_counts
current_week_counts:
    ; This function takes no parameter
    ; It returns two values:
    ; - A copy of current week's counts as a 8-byte number.
    ; - The number of days already filled in the current week, as a 8-byte number.
    ; All days after the most recent one should have its corresponding byte zeroed-out in the output
    ; At the start of the program, there is no count for the current week
    lea rcx, [rel next_bird_counts]
    mov rax, qword [rcx] 

    lea rcx, [rel day_index]
    mov rdx, qword [rcx]
    ret 

global save_count
save_count:
    ; This function takes as parameter the most recent count, as a 1-byte number ; di(l)
    ; It must save this value in a new entry for the current week
    ; If there is already 7 entries in the current week before the function is called, then:
    ; - The current week becomes the last week.
    ; - A new entry is added with the passed value in a new current week.
    ; The function has no return value

    mov rdx, [rel day_index]         ; find current index
    cmp rdx, 7                       ; if day index >= 7
    jae save_count.swap              ; ..., then 7+ values are stored
    jmp save_count.store
    ret
    
.swap:
    mov r8, qword [rel next_bird_counts]   ; get current week count as single number
    mov qword [rel last_bird_counts], r8   ; store r8 as last week count
    mov qword [rel next_bird_counts], 0x0  ; clear array
    mov byte [rel day_index], 0x0          ; clear day index
    jmp save_count.store
    ret
    
.store:
    mov rdx, [rel day_index]         ; find current index
    lea rcx, [rel next_bird_counts]  ; find address of first index

    mov [rcx + 8*rdx], dil           ; store incoming byte at location
    inc byte [rel day_index]         ; add one to the count
    ret

global today_count
today_count:
    ; This function has no parameter
    ; It returns the most recent entry for the current week, as a 1-byte number
    mov rdx, [rel day_index]         ; find current index
    dec rdx                          ; look at last counted day
    lea rcx, [rel next_bird_counts]  ; find address of first index

    mov al, byte [rcx + 8*rdx]       ; retrieve byte at location
    ret
    
global update_today_count
update_today_count:
    ; This function takes as parameter a 1-byte number ; di(l)
    ; It adds this number to the most recent entry for the current week
    ; This function has no return value
    mov rdx, [rel day_index]         ; find current index
    dec rdx                          ; look at last counted day
    lea rcx, [rel next_bird_counts]  ; find address of first index

    add [rcx + 8*rdx], dil            ; store updated count
    
    ret

global update_week_counts
update_week_counts:
    ; This function takes as parameter a 8-byte number ; rdi
    ; Each byte in the input parameter, but the last, represents a day's count in the current week
    ; The last byte in the input parameter has no meaning and must be zeroed-out
    ; This function makes the following changes:
    ; - The current week becomes the last week.
    ; - The counts in the input parameter are fully inserted in the current week.
    ret
 
%ifidn __OUTPUT_FORMAT__,elf64
section .note.GNU-stack noalloc noexec nowrite progbits
%endif
1 Like

Note that indexes are always in bytes and starting at 0. How many bytes are there in 8*rdx?

Ah. eh. rdx is 8 bytes in size, so potentially a lot.

I made my index a byte, so I should have done 8*dl instead? Nah that won’t work either. I could make it a dq, so the reads are always right. But I don’t think that will solve it.

But I think what you’re saying is that I am not writing at the right location.

1 Like