The x86-64 assembly syllabus is live

Hi, folks!

A learning mode has been added to the x86-64 assembly track.

All concepts and their exercises are in beta and we’d like to have your opinion on them!

There are more concepts planned for later, but the ones already here are probably enough to solve all practice exercises in the track.

Some observations:

  • I’m not a native English speaker, so there may be some mistakes or odd expressions here and there. If you find any, please let me know!
  • Assembly is inherently difficult, in the sense that it has very few built-in abstractions and so a lot of foundational knowledge is required even for small tasks. I’ve tried to set the track’s tone and emphasize that everything is just bytes and how we interpret them.
  • The tests are written in C, and since we can’t assume every student knows C types, I’ve tried to hide most C details to keep the focus on assembly. Once students move to the practice exercises, however, the C machinery appears in full. To help with that, I created two concepts showing how C interfaces with assembly and how to read the C code in the tests from an x86-64 assembly perspective. This means all practice exercises have one of those concepts as prerequisite. I’ll eventually circumvent this restriction somehow, probably by improving the stubs to add some hints. For now, if you want to do a practice exercise, you should finish one of those C concepts first, or switch between learning and practice mode.

A special thanks to @keiraville and @kotp for their support, patience and careful reviews!

This has been a fun project for the past month or so. I hope that it adds value to someone!

14 Likes

Well done! I’m very sad to lack the time for trying it out…

I came to programming when Assembler programming in 8 bits was the hot thing, so to me the current state of Assembly Languages is “very many abstractions and very powerful instructions” (at least in x86 CISC world). And that’s also a reason why I (for my productive work) do not want to go back below C for anything…

2 Likes

@bergjohan your “child” is growing up! Amazing stuff @oxe-b .

I will find the time to check it out. <3

2 Likes

First feedback:

Hello world

Very straightforward. No issues

Lasagna

I don’t think the heading levels are correct. This made it hard to read. Please remove Basics here so that comments, constants, functions etc. can be promoted one level.

Unknown concept “number literal”:

The snippet above stores the value 42 in all 64-bits of the rax register, which is the destination operand for the instruction.

At this moment I realised in assembly number literals exist. I do not understand the impact.


I could not use TDD to run the tests. I implemented the first function:

global expected_minutes_in_oven
expected_minutes_in_oven:
    ; This function has no arguments
    ; and must return a number
    ret 40

Ran the tests and got an issue on the second function, but realised my mistake and updated to

global expected_minutes_in_oven
expected_minutes_in_oven:
    ; This function has no arguments
    ; and must return a number
    mov rax, 40
    ret

So then I started defining all the functions with an empty ret to get to a “testable” solution. I found out the global elapsed_time_in_minutes was missing. I don’t know if that was intentional.

Finally I could run the tests


Then implementation was straightforward

global remaining_minutes_in_oven
remaining_minutes_in_oven:
    call expected_minutes_in_oven
    sub rax, rdi
    ret

global preparation_time_in_minutes
preparation_time_in_minutes:
    mov rax, rdi
    imul rax, TIME_PER_LAYER
    ret

global elapsed_time_in_minutes
elapsed_time_in_minutes:
    call preparation_time_in_minutes ; passes rdi
    add rax, rsi
    ret

Comments

  • I would have liked to see a list (table) of expected instructions (mov, imul, add, sub, and call) with description. I think it’s clear from the instructions but because it’s wordy (which is good! keep that), it was hard to refer back.
  • I do not think I learnt anything from having to write the function name with a colon followed by ret on the next line. I highly recommend the stub in the basics exercises “RUNS” (to failure).
  • Codemirror highlighting is missing. That makes it harder to progress. Looks fine as published solution

This was overal a really good experience for me.

3 Likes

Yes, I agree with you. I believe configlet generate creates those headings by default. I’ll delete them.

Spot on! It was intentional to leave it like this for now, but then explore the idea after talking about integers (because those literals, called immediates, are signed 32-bit integers). But I think in the end I didn’t talk about them at all. I’ll add something here and then an entry on the Integers concept.

To be honest, I had lots of doubts about these sort of things. Should I place lots of comments or just refer to the instructions? Should I add a table of instructions to the instructions of each exercise? Should the stub be “testable”?

In the end I decided that it’d be best to have more eyes on it and gather “live” feedback so I can adjust accordingly.

Something like that in the exercise’s description would be enough?

Instruction Description
mov copies the contents from a source operand to a destination operand
add destination operand = destination operand + source operand
sub destination operand = destination operand - source operand
imul destination operand = destination operand * source operand
call calls the function in its only operand
ret returns from a function
2 Likes

Yes, adding this is really helpful. And then in later exercises you could leave it out, or make it a list. e.g.

You will likely need the following known instructions:
mov, add, sub, imul, call, ret

The upside to this is that you can call out (Table style) new instructions. I know it’s a short list, but e.g. the jump to pointer thing or whatever it is is probably a separate exercise!

Sidenote, I personally would be fine with:

Instruction Description
mov a, b copies the contents from b to a
add a, b sets a to a + b
sub a, b sets a to a - b
imul a, b sets a to a * b
call a calls function a
ret returns from a function

It’s hard. I think for basics you want people to realise the following:

  • you can call a function
  • you cannot “pass” arguments like in other languages. Use the registers
  • using registers also makes the return value available
  • you need to structure your math to work with the register math

declaring a function and declaring it as global isn’t “the interesting part” of this exercise and because I really wanted to test if ret 40 worked, and then if mov rax, 40 worked, I think the exercise should have 0 passing tests if you “run the stub”, not a compiler error.

I felt I couldn’t do anything until I had figured out each function, but then if my understanding was incorrect, would not be able to figure out how to solve it.

2 Likes

Integers

I’ll write down my exact thought process as I complete this so you can see if something is not how you expected that to be.

When I saw the stub, I knew I first had to do “the boilerplate work” to make it runnable:

; Inventory Management

section .text

global get_box_weight
get_box_weight:
    ; This function takes the following parameters:
    ; - The number of items for the first product in the box
    ; - The weight of each item of the first product, in grams
    ; - The number of items for the second product in the box
    ; - The weight of each item of the second product, in grams
    ; The function must return the total weight of a box, in grams
    ret

global max_number_of_boxes
max_number_of_boxes:
    ; This function takes the height of the box, in centimeters, as parameter
    ; It must return how many boxes can be stacked vertically
    ret

global items_to_be_moved
items_to_be_moved: 
    ; This function takes the following parameters:
    ; - The number of items still unaccounted for a product
    ; - The number of items for the product in a box
    ; The function must return how many items remain to be moved, after counting those in the box
    ret

global calculate_payment
calculate_payment:
    ; This function takes the following parameters:
    ; - The upfront payment
    ; - The total number of boxes moved
    ; - The number of truck trips made
    ; - The number of lost items
    ; - The value of each lost item
    ; - The number of other workers to split the payment/debt with you
    ; The function must return how much you should be paid, or pay, at the end
    ret
    
%ifidn __OUTPUT_FORMAT__,elf64
section .note.GNU-stack noalloc noexec nowrite progbits
%endif

I then started reading the instructions (temporarily editing the style of h4 to be h3).

Even if you fix the format, this will not look correct in the UI. I recommend adding *_italicbold_* to all the level 5, then level 4 headings (in markdown level 3 after you fix it).

Now Proposed
image image

Lots of interesting information about different forms of multiplication, but I am wondering if I’ll need it in this exercise. I’ll keep reading though. I like the caution admonitions. Very clear stuff, in very little text. Thanks!

Would love the summary table of instructions at the bottom like mentioned before.

The actual exercise

Okay, so I have a function with 4 parameters. I do not think it’s reasonable to expect I recalled which register is used for which parameter. I now have to go back to the previous exercise to get this information.

This track uses the System V AMD64 ABI calling convention and the six first integers arguments are passed to a function in registers. They are passed in the following order: rdi , rsi , rdx , rcx , r8 , and r9 .

It could be helpful to link back to the previous concept in the structions, using a concept link (see docs on exercise how to do that, I do not know of the top of my head, but we sometimes use it in JS).

Trying that.

I see in the instructions

All arguments are 16-bit non-negative integers, and the return value is a 32-bit non-negative integer.

So that means that I likely need to use imul, and I likely need to “pad” those numbers (movezx?). The warning about this was relevant for 32bit numbers, but I am dealing with 16bits, right? But if I multiply, it may overflow into 32 bits…

Using imul with unsigned numbers […]

Okay so maybe I use the example from the instructions about extending to 32 bits first, and then multiplying.

movzx eax, rdi ; lower 16 bits of rdi, upper bits are cleared
movzx ecx, rsi ; lower 16 bits of rsi, upper bits are cleared
mul ecx        ; result should now be in eax

That doesn’t work. Compiler error: inventory_management.asm:18: error: invalid combination of opcode and operands. Perhaps I need to use the 16-bit registers di and si. I got this from the table from the first exercise, but there is zero explanation about this.

movzx eax, di ; lower 16 bits of di, upper bits are cleared
movzx ecx, si ; lower 16 bits of si, upper bits are cleared
mul ecx       ; result should now be in eax

That doesn’t error. I will move ecx into the return value. My guess is that rax would not work (size?), but let me just move it into esp so I can do the other multiplication as well.

mov esp, eax

Yeah no. That doesn’t work and it doesn’t tell me why.

Segmentation fault (core dumped)

Okay, then I guess I’ll try to just move eax into rax:

mov rax, eax

And I get the original error again: inventory_management.asm:23: error: invalid combination of opcode and operands, despite the instruction saying that this should be possible:

A 32-bit source operand is always zero-extended to all 64 bits of the destination operand with a simple mov.

Wait, perhaps rax, eax, ax are the same thing, but just accessing it differently. Back to the original instructions

Illustration of how the bits are accessed for the rax register:

Yes! Okay great. So the result is already in the right register. I just need to move it out so I can do the second multiplication, then sum them. Golden!

mov ebx, eax

movzx eax, dx ; lower 16 bits of dx, upper bits are cleared
movzx ecx, cx ; lower 16 bits of cx, upper bits are cleared
mul ecx       ; result should now be in eax

add eax, ebx

But that won’t work because the first multiplication “used” the registers that I now need. I didn’t find a way to make mul use a different second register, but I can inverse the operations so that I “clear out” parameter 3 for multiplication usage.

I am off by 500g , but that is solved because the empty box is 500g.

max_number_of_boxes

I see that the comment about the parameter is structured differently here. Would be good to change for consistency of the other stubs.

; This function takes the following parameters: 
; - This function takes the height of the box, in centimeters, as parameter
; The function must return how many boxes can be stacked vertically

I see I need to do a division, namely:

Divide truck height by the first parameter.

I read that idiv will divide whatever is in rdx:rax with the given argument.

@oxe-b told me that the constant literals are 32 bits, so I should mov it into a 64-bit register if I want everything to work on 64 bits:

mov rax, TRUCK_INTERIOR_HEIGHT
idiv rdi; first parameter

And I hit an error:

make: *** [Makefile:29: all] Arithmetic exception (core dumped)

Maybe it’s because I ignored this part in the instructions:

So, whenever working with 64-bit integers, all bits in the rdx register must be cleared for a non-negative number in rax , and set for a negative number.

I mean, I am, I guess, working with 64 bit integers, but maybe I should just limit myself to 32bits? All the content that follows below this warning do not tell me how to clear the bits, and are all about sign extending, but I don’t want that. I will take the following approach:

  • move <> 0 into both registers to clear it (??)
  • If that doesn’t work, maybe try movzx <> 0 (??)
  • If that doesn’t work, don’t use r registers, but use the 32 bit equivalents so I can ignore all this (??)
mov rax, 0
mov rdx, 0

mov rax, TRUCK_INTERIOR_HEIGHT
idiv rdi; first parameter

Okay, that worked. Maybe it was just rdx not being set, because I read that TRUCK_INTERIOR_HEIGHT would be zero extended. I retry without the first mov rax, 0.

items_to_be_moved

I find the instructions hard to follow, but I think it wants me to subtract the second argument from the first.

The second argument is in rsi, the first one in rdi.

sub rdi, rsi 

That doesn’t work. It only gives me negative numbers. The instructions point that this may be an error:

The arguments are 32-bit non-negative integers. The return value is a 32-bit integer. In case of an error in the process, it is possible that the result is a negative number.

Perhaps I should use the 32 bit registers instead. edi and esi it is.

sub edi, esi

I am still getting negative numbers.

I tried to figure out what the tests are using, so I tried sub edi, 0 and sub esi, 0 and sub rdi, 0, and then realised: the result is in the first argument, so I still need to move it to rax.

sub edi, esi
mov eax, edi

Yay. That worked. Now I am wondering: is it idiomatic to move the argument into eax and end with sub eax, esi, or is it idiomatic to move the result explicitly into eax. And should I have used the 64 bit integer registers?


Do you want me to continue this? Does that help?

Key takeaways solving the first task:

  • Lack of locality of knowledge. Link back to previous content would help.
  • I recommend adding a “info” admonition: remember you can access the same register with different sizes using the correct register name, eg. rax for 64 bits, eax for 32 bits, ax for 16 bits.
  • I recommend adding parameter comment for max_number_of_boxes.
  • I recommend adding a “caution” at the first instruction that mul uses ecx, and that the third argument is also in (e)cx. I don’t think it was fun or helpful to have to figure this out. You still need to think about the solution anyway (am I wrong here?)
  • It is unclear how to clear a register. I guessed (correctly? I have no way to check)
  • It is not obvious when you should use one register type over another, except that usually both should be the same size.
  • I will report back when I did the other tasks.
3 Likes

@SleeplessByte

Thank you very, very, much for your input. I have just merged a PR with lots of changes based on your suggestions. Can you update the exercise to check if things are clearer now?

I now have a better picture on how a student might face those concepts. I’ll add tables and comments on many other exercises later.

3 Likes

I’ll write the troubles I am having with the last part of the integers exercise, as it may be interesting to see what notes you can add to help with the didactic process to that as well (I am stuck XD)

1 Like

@oxe-b Want to quickly chime in here and say that I am super-impressed and really excited to see this work getting done! I’ve been Assembly-curious for a long time, but very very intimidated. And while I really don’t have time to dig in right now (as much as I really really want to!!), I did want to say GREAT WORK. :blue_heart:

5 Likes

Integers (cont.)

calculate_payment

I start with defining constants for 5 and 220. It is unclear to me if I can declare constants inside the “text” section, and if that is allowed/idiomatic, so for now I will put them at the top.

PAYMENT_PER_BOX equ 5
PAYMENT_PER_TRIP equ 220

Hmmm I don’t find the instructions particularly clear about lost items. I either need to multiply The number of lost items by The value of each lost item or I don’t understand, but I am going to work with that for now.

(see response for remainder)

One of the operands is 64 bits, so everything needs to be.

I will do the following:

  • Calculate expected lost boxes
  • Calculate trip payments
  • Calculate box payments
  • Calculate payment left by
    1. Adding trip payment to box payment
    2. Subtracting lost and damaged
    3. Subtract total from made payment
  • Divide by workers (and do something with remainder)
    ; Calculate expected lost boxes 
    mov rax, r8                  ; lost value per item
    mul rcx                      ; multiply with count items, result should now be in rax
    mov r10, rax                 ; temporarily store in r10

    ; Calculate trip payments
    mov rdx, PAYMENT_PER_TRIP    ; per trip
    mul edx                      ; multiply with trip count, result should now be in rax
    mov r11, rax                 ; temporarily store in r11

    ; Calculate box payments
    mov rdx, PAYMENT_PER_BOX     ; per box
    mul esi                      ; multiply with box count, result should now be in rax
    mov r12, rax                 ; temporarily store in r12

    ; Calculate payment left by
    ; 1. Adding trip payment to box payment
    ; 2. Subtracting lost and damaged
    ; 3. Subtract total from made payment
    
    add r11, r12
    sub r11, r10
    sub rdi, r11

    ; Now how much does everyone get? Division
    ; ??

    ; Move answer to return value
    mov rax, rdi
    ret

This yields incorrect answers. What I have seen before is that this happens often when I am not clearing bits, but because there is no log, I have no good way to debug this, as the number of operations here is high, so I don’t know yet how to approach debugging it.

Issue 1

I was incorrectly “ovewriting” my rdx register. Starting with the payment per box solves this:

; Calculate trip payments
mov rax, PAYMENT_PER_TRIP    ; per trip
mul rdx                      ; multiply with trip count, result should now be in rax
mov r11, rax                 ; temporarily store in r11

Now I get 1100 for 5 trips (which is correct).

Issue 2

I was incorrectly placing PAYMENT_PER_BOX into rdx. Fixing that yields the correct result:

 ; Calculate box payment
mov rax, PAYMENT_PER_BOX     ; per box
mul rsi                      ; multiply with box count, result should now be in rax
mov r12, rax                 ; temporarily store in r12

Now I get 5000 for 1000 box (which is correct)

Issue 3

To make it easier for myself, I will do all calulcations on rax

; Calculate payment left by
; 1. Taking 0 to be paid
; 2. Adding trip payment to be paid
; 3. Adding box payment to be paid
; 4. Subtracting lost and damaged
; 5. Subtracting made payment

mov rax, 0    ; 0
add rax, r11  ; 0 + 1100
add rax, r12  ; 0 + 1100 + 5000
sub rax, r10  ; 0 + 1100 + 5000 - 42
sub rax, rdi  ; 0 + 1100 + 5000 - 42 - 2000

This gives me 4058 which is correct because I need to split this with the one worker that helped me, which is the final step.

Let’s start with the simple case. Need to add 1 to the number of workers (me) and then do a division. The number may already be negative, so I need to use idiv and not div.

; Now how much does everyone get?
; Worker count plus me
add r9, 1
movzx r9, r9b
    
idiv r9

This works for the first test case, but not for the second:

Expected -794 Was 3689348814741909529. The function was called with arguments: 2000, 598, 2, 120, 45, 4.

This is most likely a sign-issue. I will look into that later.


So, whenever working with 64-bit integers, all bits in the rdx register must be cleared for a non-negative number in rax , and set for a negative number. This is called sign extension .

That sounds likely of what’s happening. Imma try these things out. I think that I basically need to say: copy the sign from rax to rdx so that when rdx is used, the sign is correct. cqo gives me that.

; Now how much does everyone get?
add r9, 1          ; add me to total count of people
movzx r9, r9b      ; zero-fill people count to 64 bits
cqo                ; extend sign from rax to rdx
idiv r9            ; divide

Yay. That worked. One more test to go.

Expected 2123 Was 2035. The function was called with arguments: 57412, 163214, 183, 1931, 185, 216.

That’s clearly a remainder issue.

add rax, rdx       ; add remainder to result

Key take-aways

  • After the edit by @oxe-b both solvable and pretty fun to solve
  • It’s key to understand what the expected values are
  • It’s key to understand you can return early
  • After I solved the exercise I read the hints. I do not think most of them are particularly good. For example, the hints for the final exercise are all about sign extending but you won’t need it until you do imul, so the order is wrong. I would expect the following hints:
    1. You can return early to check each step
    2. You can use the extra registers rn to store temporary values
    3. There is an instruction for signed division.
    4. If you get really big answers, when a negative is expected, it’s likely the sign of one of the number is incorrect.
    5. There is an instruction for sign-extending values from rax into rdx.
    6. The payment is split between you and a certain number of workers and you also get the remainder.
    7. The remainder of a division is stored in rdx.
2 Likes

image

Yes, this helps a lot. Please note that you need to wrap the constant names in a backtick or it will become italic.

This is particularly helpful. I can now ret early, and see my progress. This makes it solvable. I will report (and edit) the previous message after I work out my stupidity.

(I did 21 * 2, btw, which is 42, so yay)

2 Likes

Conditionals (Black Jack)

Same stuff about the format of the markdown files, of course :slight_smile:, and I first go through the stub, adding register names, and minimal implementation of function name: ret

Interestingly, without any implementation, these 6 tests pass. Given that rax is not set, and the argument is passed in via parameter register di / dil, I don’t understand why this happens, but I will ignore it.

There’s a special register called rflags. Its bits act like flags for various conditions.

Yeah okay, but can I set them by doing mov CF, 1? Unclear how to set or read the flags. The next section goes into it (albeit not being necessarily a full answer to my question). I think I would have liked:

“For the purpose of this concept, you never manually set these flags. They are set by specific instructions with specific operands, such as cmp and test.”

(and then the cmp and test sections follow).

value_of_card

I think this wants me to return the passed in value, unless it’s CA (ace), in which case it should become 1, or a face card (CJ, CQ, CK) in which case it should become 10.

I could have 4 “comparisons”. I could also first check if it’s a pip card, then check if it’s ace, leaving me with only 2 comparisons:

- test value <= 10   -> return self
- test value == 14   -> return 1
- -> return 10

The comparison can be used and then CF can be checked for the first case, but if I read correctly, I can cmp twice and rely on two flags:

cmp A, B

CF	A < B (unsigned)
ZF	A == B

Resulting in:

cmp dil, CJ   ; sets CF to 1 if card < 11 (2 through 10)
cmp dil, CA   ; sets ZF to 1 if card = 14 (Ace)

I know I can then jump based on these flags. The jcc explanation is hard to read. What I am missing here are examples of how to use it.
Unfortunately the entire section lacks examples on test usage as well. So I will need to “be smart” about this.

It is listed that s suffix tests SF and z tests the ZF flag. I don’t know what g means as suffix, because it sounds like greater, but that would mean it’s testing for ~(CF | ZF | SF) (none of these are set). Is that correct? I don’t know, because it’s not explained. Anyway, ignoring that for now.

If I understand correctly, I can do the following:

jc pip  ; CF is 1, so it's a pip card
jb pip  ; a < b, so it's a pip card

jz ace  ; ZF is 1, so it's an ace
je ace  ; a = b, so it's an ace

With some local labels, that gives me

; the global directive makes a function visible to the test files
global value_of_card
value_of_card:
    ; This function takes as parameter a number representing a card
    ; - passed in card ; di(l)
    ; The function should return the numerical value of the passed-in card

    cmp dil, CJ           ; sets CF to 1 if card < 11 (2 through 10)
    jc  value_of_card.pip ; CF is 1, so it's a pip card

    cmp dil, CA           ; sets ZF to 1 if card = 14 (Ace)
    je  value_of_card.ace ; ZF is 1, so it's an ace

    ; else it's a face card
    mov al, 10
    ret
    
.pip: 
    mov al, dil
    ret

.ace:
    mov al, 1
    ret

This works.

Things I would like

  1. Rewritten Conditional Jump section. I think start with some examples, and then tell me what is idiomatic. Should I use je or should I use jz? Or do people pick what they like? I would love to “just” see a table of boolean operators and bitwise operators and their jcc equivalent.
  2. Example with local label! It was unclear that I needed function_name.local_name, but I guessed correctly. I think the rest of the content there was fine and understandable, but an example here would go a long way.
  3. Should I clear rflags before doing anything? When is it cleared? When calling a function? I don’t know. I guess I’ll mov rflags, 0 just to be certain? Oh wait, I cannot: black_jack.asm:31: error: symbol 'rflags' not defined. Does this matter?

higher_card

I think this is what I need to do:

  • get the value for both cards
  • compare the value of both cards
  • return the original cards when bigger

I think I can use a smart jump schema with a single CMP:

cmp cl, al
jb higher_card.lt    ; cl < al
ja higher_card.gt    ; cl > al
                     ; cl = al

I hit a roadblock. @oxe-b , the returned value is not shown, so I cannot see where I make a mistake in this exercise. Example:

Test Failure
The function was called with arguments: C4, C8.

Yeah, but what am I returning?

Anyway. I was able to guess correctly where my mistake was. I now have two local labels:

.lt: ; value of dil < value of sil
    mov al, r9b ; return original sil
    mov dl, 0   ; ensure cleared
    ret

and one default case:

; Return both
mov al, r8b
mov dl, r9b
ret

value_of_ace

I think this one won’t be hard.

  1. Check if first parameter is equal to CA, and jump if so
  2. Check if second parameter is equal to CA, and jump if so
  3. Convert both parameters to their value
  4. Sum their value
  5. Check if sum > 10, and jump if so
  6. Return 11

If jumped

  1. Return 1

No notes on this one. I felt very good when the knowledge of the other functions came together here.


is_blackjack

I think what they want me to do is to compare “both” cards, and it explicitly says not to sum. We don’t need to convert anything here. What I will do is:

  • Is the first card an ACE, then I need the second to be a Face
  • Is the first card a Face, then I need the second to be an ACE
  • else, it’s not
cmp dil, CA
je  is_blackjack.needs_face

cmp dil, C10
jg  is_blackjack.needs_ace
jmp is_blackjack.false

And then each local label has a similar implementation, where needs_face needs two checks (CA =, and > C10), and needs_ace only needs one.

No notes on this one.


can_split_pairs

This reads like: convert to value, then compare.

Whilst I like solving these quickly, it does seem like these are easier than everything before, but I think that makes sense and we can not do something about it.

I am wondering though, is this the canonical / idiomatic way to do it? If only I could read out the flag directly, then I did not need the jump!

    cmp r8b, r9b
    je can_split_pairs.yes_we_can

    mov al, FALSE
    ret
    
.yes_we_can:
    mov al, TRUE
    ret

can_double_down

Check if the sum is 9, 10, or 11. The easier way is to check if < 9 and check if > 11.

Fun stuff. <3


Takeaways

  1. Some doc changes
  2. Better test messages (please tell me what I am returning)
1 Like

Thank you very much! I can’t update the docs right now, but I’ll get to it as soon as possible.

rflags is a special register, which means “do not use directly, it changes in controlled way through a few sanctioned instructions”. It is mentioned here just so the flags are not too abstract. The student should interact with it only through cmp and test in this concept.

I was a bit divided about leaving test here because bitwise ops are in a separate concept. I think I’ll move it to the other concept to focus only on cmp here.

Those suffixes such as z, l and g are aliases to some flag set or cleared. Those aliases come in two “flavors”, some refer to the flag specifically (so z in jz refers to ZF), and some refer to their meaning as result of cmp. You can think of cmp as like of a generic comparison operator, a <=> of sorts.

So je == jz, because in cmp a, b the ZF is set when a == b (and so e refers to “equal”).

Inside the cmp aliases:

  • l is less, g is greater, n is not. And things compose: jle is less or equal; jnge is not (greater or equal).

  • l and b are similar, l being for signed comp and b for unsigned. Same for g and a.

As for the flag aliases, only n composes: jnz, jnc, etc.

You can use whatever alias you want for the job. There is no more idiomatic here. For example, jc == jnae because CF is set when a < b in unsigned comp (and a < b == !(a >= b) ).

It seems I’ve failed to communicate all of this in the concept. I’ll rework the text.

1 Like

(I got through it so overal the content is fine, but I am explicitly being verbose with my thinking so you can make the content even better!)

Yes, perfect. I think a small addition to the instructions solves all my questions in that regard.

Yes, I think this should be in some bitwise operators exercise. I was confused by it down the line as well.

This seemed implied, but was not said explicitly. It reads like: “there is a finite number of combinations, here are a few that make sense, try them out yourself”. Could be solved by a table or what you just wrote here, which explains it pretty well.

Color Palette (Memory)

First think I notice is that the new concepts are about .rodata and setting “data”. However, before I have been using equ and here it’s dx, without explanation how equ is similar or not to that. It is also unclear to me if I can combine global calls, and if they need to precede the declaration.

I ended up with:

section .rodata
    global RED
    global GREEN
    global BLUE
  
       RED dd 0xFF000000
     GREEN dd 0x00FF0000
      BLUE dd 0x0000FF00

section .data ; mutable data
    global base_color

base_color dd 0xFFFFFF00

…which seems to work (4 passing tests).

get_color_value

When I read the instruction I am reading: “take the address, deref it, return it”. So I do:

mov rax, [rdi] ; this dereferences the address passed in

I should add the dword size (32 bit), and then it fails. This likely happens because the size is incorrect, and it should be eax.

mov eax, dword [edi] ; this dereferences the address passed in

This unfortunately errors. I re-read the instructions and I see that nowhere it tells me how big an address is. I’ll just guess, as rdi works, that I need to use rdi here (64 bit), but this part is unclear.


add_base_color

Instructions indicate that I basically need to move the value of the given address (argument in the first parameter) into the mutable data variable base_color.

Nowhere in the instructions does it tell me how to accomplish that:

I try the following permutations:

call get_color_value
; Now eax has the value I need

mov base_color, eax      ; I expected this to work
lea base_color, [eax]    ; I expected this to work

Both give me the same error:

color_palette.asm:35: error: invalid combination of opcode and operands

None of the following work either:

mov base_color, rax
mov base_color, [rax]
mov base_color, [eax]
lea base_color, rax
lea base_color, eax
lea base_color, [rax]

Sooooo I am stuck again @oxe-b !

In order to access (read or write) the contents of a memory location, it is necessary to dereference its address.

First I will try this directly:

mov [base_color], eax
/usr/lib/gcc/x86_64-alpine-linux-musl/12.2.1/../../../../x86_64-alpine-linux-musl/bin/ld: color_palette.o: relocation R_X86_64_32S against `.data' can not be used when making a PIE object; recompile with -fPIE
/usr/lib/gcc/x86_64-alpine-linux-musl/12.2.1/../../../../x86_64-alpine-linux-musl/bin/ld: failed to set dynamic section sizes: bad value
collect2: error: ld returned 1 exit status
make: *** [Makefile:32: tests] Error 1

hehe. Okay. Next I will try to dereference the current value in a register, and then move something into that?

mov esi, dword [base_color]
lea esi, [eax]

Same error!

Perhaps:

mov dword [base_color], eax

Hmmm. The error says something about PIE and I saw something about that in the instructions.

All exercises in this track are compiled and linked as PIE, so rel should be used to generate relative addresses.

mov dword [rel base_color], eax

:partying_face::tada::confetti_ball: rejoice!


make_color_combination

I had to re-read the instructions a few times, but I need to import combining_function`

section .text

extern combining_function 

Then I need to use it.

This function takes as parameters the 32-bit values for base_color and for a secondary color to be mixed with it. It returns the 32-bit value for the combined color.

In other words:

  • Add to edi the base_color
  • Add to esi the secondary color
  • Read results in eax

Finally I need to figure out what needs addressing and what does not.

; First store the address for later
mov r8, 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

This runs, so what I am doing is not completely incorrect. I now expect the resulting colour to be in eax.

But now the fun begins with another error that’s hard to figure out.

make: *** [Makefile:29: all] Segmentation fault (core dumped)

My thinking is:

  • eax is the actual 32-bit value returned (per the instructions).
  • r8 is the address where I need to store it
  • In order to store it, I need to do the mov dword [r8], eax from before

Unfortunately that doesn’t work :(

  1. r8 Is not a relative address, because it came in as an argument from outside, and I get a different error when I try.
  2. Perhaps I do need to [eax], but mov [r8], [eax] tells me the sizes don’t agree (yeah need r8d, not r8).
  3. Trying [r8d], [eax] gets me back to color_palette.asm:56: error: invalid combination of opcode and operands.

:boom:

(Cont: The x86-64 assembly syllabus is live - #23 by SleeplessByte)

1 Like

I may have failed to communicate one of two things, but I’m not sure which:

  1. The name of a variable is actually the address for a memory location where it is stored.

  2. In order to access (read or write) the contents of a memory location, it is necessary to dereference its address.

I’ll also make it clear that all addresses in x86-64 are 64-bit values.

P.S.: I’m starting to think I’m not a good concept writer lol

I think you are actually good, the only thing you’re missing is a total beginner like me, who points out the parts that are “obvious” if you use assembly (or similar), but not if you have not used it yet.

Note that I am only pointing out “that part isn’t clear”, and not “what’s written here is not understandable”.

In order to access (read or write) the contents of a memory location, it is necessary to dereference its address.

Ah. Okay. I will try to put the [] on the left side, and see what happens. I will put my findings in the reply above. (Don’t tell me yet if that’s right or wrong, i wanna try haha)

(I got there, see above)

1 Like

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