_If you were ever curious but were confused by the youtube tutorials. I'm going to try to make this as concise and useful as possible. Also, x86 is way easier to code in than ARM and if you disagree, you're wrong. And Intel syntax is better than AT&T syntax_↵
↵
_This assumes you have a basic understanding of how pointers work in C_↵
↵
First program & compiling↵
==================↵
↵
Make a file named `hello.s`:↵
↵
~~~~~↵
.intel_syntax noprefix↵
↵
.section .rodata↵
mystring:↵
.ascii "Hello, World!\n"↵
mystring_end:↵
↵
.section .text↵
.globl main↵
.type main, @function↵
main:↵
endbr64↵
push rbp↵
mov rbp, rsp↵
↵
mov eax, 1 # write↵
mov rdi, 1 # stdout↵
lea rsi, mystring[rip]↵
mov rdx, mystring_end - mystring↵
syscall↵
↵
xor eax, eax↵
↵
leave↵
ret↵
~~~~~↵
↵
Compile and run with↵
↵
~~~~~↵
$ gcc -o hello hello.s↵
$ ./hello↵
~~~~~↵
↵
In assembly, you are writing CPU instructions, so you only have access to low-level things:↵
↵
- Registers: the main ones are `rax`, `rbx`, `rcx`, `rdx`, `rsi`, `rdi`, `rbp`, `rsp`, and `r8...r15`↵
- Memory (dereferencing pointers, writing to the stack, etc.)↵
- System calls↵
↵
The C stack↵
==================↵
↵
Your CPU only knows how to execute instructions, so how do we have functions be able to call other functions or themselves? The answer is the stack: each function has its own stack frame. For example, if `main` has its stack frame, and if it calls `printf`, then a new stack frame will be created for `printf`. Once `printf` is done executing, we will pop `printf`'s stack frame and go back where we left off to `main`.↵
↵
I'm now going to explain the actual nitty gritty of how this works; it may be a bit hard to follow along so if you're confused please let me know! I think it is fun to understand this though.↵
↵
Two registers are dedicated to managing the stack: `rbp`: **b**ase **p**ointer, and `rsp`: **s**tack **p**ointer. The base pointer points to the beginning/base of the stack frame, while the stack pointer points to the end. Also be aware that in x86, the stack grows _downward_, i.e., the `main` stack frame will be at a higher address in memory, and as more data is pushed onto the stack, the address _decreases_.↵
↵
Here's an example of how it works in practice.↵
↵
Suppose your code section looks like this (remember that code/instructions are also loaded in memory)↵
↵
~~~~~↵
address instruction↵
main:↵
...↵
5598 load pointer corresponding to format string into rdi↵
55a0 load integer into rsi↵
55a8 call printf↵
55b0 set exit code to 0↵
55b8 leave↵
55c0 ret↵
...↵
printf:↵
57d8 endbr64↵
57e0 push rbp↵
57e8 mov rbp, rsp↵
57f0 random instruction that does printing↵
57f8 another random instruction that does printing↵
5800 yet another random instruction that does printing↵
5808 leave↵
5810 ret↵
~~~~~↵
↵
So, we're currently on the stack frame of `main()` and we're about to execute the instruction `call printf` at `55a8`. After we're done executing `printf`, we need to execute the instruction at `55b0`. So, we push the address `55b0` onto the stack. This is called the **return address**, the CPU will jump to `55b0` after it's done executing `printf`.↵
↵
Now we jump to the address of `printf`, `57d8`. The first instruction is always `endbr64`, without getting too deep, it's a security feature; if you try to call a function and the first instruction is not `endbr64`, the CPU will segfault (if the security feature is enabled).↵
↵
Then, `push rbp` pushes the previous (`main`'s) base pointer to the stack. Then `mov rbp, rsp` makes `rbp` point to the beginning of the new stack frame, which is the current `rsp`. Note that the CPU automatically moves `rsp` to the end of the stack whenever anything is `push`ed or `pop`ed.↵
↵
The stack will look something like:↵
↵
~~~~~↵
address data↵
7ffe6590 <random data in the stack for main, like main's local variables>↵
7ffe6588 <random data in the stack for main, like main's local variables>↵
7ffe6580 <random data in the stack for main, like main's local variables>↵
7ffe6578 <return address telling us where to go back to in main()>↵
7ffe6570 <main's rbp value saved> # rbp now points to 7ffe6570↵
7ffe6568↵
7ffe6560↵
# rsp always points to the end of the stack, which is `7ffe6570` for now, but it may move down if `printf` has local variables, etc. that it wants to put on the stack.↵
~~~~~↵
↵
Eventually, `printf` will finish executing. The `leave` instruction is kind of like a macro — it will dereference `rbp` and set `rbp` to that value. `rbp` is a pointer to the beginning of the frame but it also points to `main`'s previous `rbp` value (see the figure above at `7ffe6570`). So this restores `rbp` back to its previous value when we go back to `main()`. It also automatically resets the stack pointer to the beginning of the frame, which effectively "deletes" everything in the current stack frame.↵
↵
Finally, `ret` will read the return address, which is now the last thing in the stack, and jump to the instruction we were previously at. The `rbp` and `rsp` registers are restored to the same places that they were before `printf` was called.↵
↵
Arithmetic operations↵
==================↵
↵
You can perform arithmetic operations on registers. For example, `add eax, ecx` performs `eax = eax + ecx`.↵
↵
Note that the letter you prefix a register with denotes its size: `rax` is the full 64-bit register, `eax` is the lower 32 bits of the register, `ax` is the lower 16 bits of the register, and `ah` is the upper byte of `ax`, and `al` is the lower byte of `ax`. The same thing applies for `rbx`, etc. For `r8` through `r15`, it's `r8` for full size, `r8d` for 32 bits, `r8w` for 16 bits, and `r8b` for 8 bits.↵
↵
So `add eax, ecx` performs 32-bit addition and `add rax, rcx` performs 64-bit addition. For addition and subtraction, the same instruction does both signed and unsigned addition/subtraction because of some magic in the representation of integers called 2's complement.↵
↵
For multiplication, it's a bit more complicated: `mul rxx` multiples `rax` by `rxx` and places the lower 64 bits in `rax` and upper 64 bits in `rdx`. If you do `mul exx`, it multiplies `eax` by `exx` and places the lower 32 bits in `eax` and upper 32 bits in `edx`.↵
↵
That's unsigned multiplication. Signed multiplication is `imul`.↵
↵
For division, it's also a little weird. `div rxx` takes the 128-bit value `rdx:rax` and divides it by `rxx`, places the quotient in `rax` and remainder in `rdx`. So if you want to do 64 bit division, you have to make sure to zero out `rdx`. Then `idiv rxx` is the same but for signed division. You may need to sign extend: `cqo` sign extends `rax` to `rdx:rax`, and `cdq` sign extends `eax` to `edx:eax`. (Sign extension is required if you're dealing with negative numbers.)↵
↵
The multiplication and division stuff is a bit complicated, but luckily we're in the age of AI so you don't have to memorize this.↵
↵
Moving data around↵
==================↵
↵
You've probably seen the move instruction: `mov rax, rdx` copies `rdx` into `rax` (and similar for 32-bit register views).↵
↵
To move between registers and memory, you can do something like `mov DWORD PTR [rbp-8], 572`. `[rbp-8]` means dereference the pointer `rbp-8`, so we're dereferencing the location 8 bytes below the base of the current stack frame. `DWORD PTR [rbp-8]` means we're interpreting it as a double word (32 bits). We also have `QWORD PTR` (64 bits), `WORD PTR` (16 bits), and `BYTE PTR`. So this writes the number `572` into the 32-bit integer located at `[rbp-8]`.↵
↵
Here are a few more examples. Can you tell what they mean?↵
↵
1. `mov eax, DWORD PTR [rbp-20]`↵
2. `mov QWORD PTR [rbp-8], rbx`↵
↵
<spoiler summary="Spoiler">↵
1. Load 32 bits at `[rbp-20]` in memory into `eax`.↵
2. Store the 64-bit `rbx` at `[rbp-8]` in memory.↵
</spoiler>↵
↵
If you look at assembly generated by GCC, you'll see a lot of moves relative to `rbp`. That's because we like to reference variables by their location relative to the base of the stack frame. For example, if you declare `int a, b, c;` in a function, GCC may decide to place `a` at `[rbp-4]`, `b` at `[rbp-8]`, and `c` at `[rbp-12]`.↵
↵
We also have `lea` for **load effective address**. It's typically useful for pointer arithmetic (you can also just use `add`/`sub` but `lea` is typically more idiomatic and faster).↵
↵
For example,↵
↵
~~~~~↵
# This loads `rbx + rax` as an address into `rdi`. It's equivalent to `rdi = rbx + rax` so you can technically replace it with `add` instructions. In practice this could mean that `rbx` was a pointer to an array of chars and `rax` is the index, so this is `&rbx[rax]`.↵
# Note that we have brackets here but the memory is not actually dereferenced.↵
lea rdi, [rbx + rax]↵
↵
# If `rbx` is a pointer to an array of 4-byte ints, then this is like `&rbx[rax]`.↵
# Note that you can't just put arbitrary arithmetic here, lea only allows `[base + size*index + offset]`. And size can only be 1, 2, 4, 8.↵
lea rdi, [rbx + 4*rax]↵
↵
# If `rbx` is a pointer to 8-byte structs, and `field` is located at a 3-byte offset in the struct, then this is like `&rbx[rax].field`.↵
lea rdi, [rbx + 8*rax + 3]↵
~~~~~↵
↵
Sections↵
==================↵
↵
There are `.section` directives. The sections are:↵
↵
- `.text`: code↵
- `.data`: global variables, that can be pre-initialized to a certain value↵
- `.bss`: global variables, that are automatically zero-initialized↵
- `.rodata`: read-only constants, such as literal strings in C↵
↵
### Global variables↵
↵
This in C:↵
↵
~~~~~↵
int32_t y = 1000;↵
int64_t z = 67;↵
char s[] = "forcescode";↵
~~~~~↵
↵
Is this in assembly:↵
↵
~~~~~↵
.section .data↵
↵
.globl y↵
y:↵
# Also .byte, .short↵
.long 1000↵
↵
.globl z↵
.align 8 # Unaligned data will not cause errors in modern x86_64, but it is slower.↵
z:↵
.quad 67↵
↵
.global s↵
s:↵
# .string or .asciz null-terminate the string, .ascii does not↵
.string "forcescode"↵
~~~~~↵
↵
Note that `.globl` is a directive for it to be visible to outside files or not. It's like static vs non-static in C.↵
↵
For bss, you just reserve space:↵
↵
~~~~~↵
.section .bss↵
↵
.globl myarray↵
myarray:↵
.zero 24000 # 24,000 bytes↵
~~~~~↵
↵
### String literals↵
↵
Typically, you'll put these in rodata:↵
↵
~~~~~↵
.section .rodata↵
↵
mystring:↵
.string "codeforces"↵
~~~~~↵
↵
Also, typically when referencing data or bss values, you need **instruction pointer relative addressing**. Because your code needs to be position-independent, you can't have a fixed address to reference the global variable, you need it relative to `rip`, the instruction pointer register.↵
↵
~~~~~↵
# Equivalent↵
lea rdi, mystring[rip]↵
lea rdi, [rip + mystring]↵
~~~~~↵
↵
Both of these reference `mystring`, but as an offset relative to `rip`. At link time, the offset of `mystring` relative to `rip` is calculated, and then the code will correctly reference `mystring`.↵
↵
Basic calling convention↵
==================↵
↵
I'll probably go in more detail on this in part 2. For now, here's the basics of how to call other functions correctly.↵
↵
You pass the first **6** arguments in registers: `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`. The classic way to remember it is " **Di**ana's **si**lky **d**ress **c**osts **89**".↵
↵
Additionally, for variable argument functions like `printf` or `scanf`, you also need to zero out `eax`. (I'll explain why later.)↵
↵
For example, if you wanted to `printf("The answer is: %d\n", answer)`, you would↵
↵
~~~~~↵
lea rdi, location_of_string[rip] # Assuming the string literal is in .rodata↵
mov esi, [pointer to variable `answer`]↵
xor eax, eax # "Better" way to zero out a register than `mov eax, 0`↵
call printf↵
~~~~~↵
↵
Then, the function's return value is given in `rax`/`eax`/`ax`/`al` (depending on if the return value is a 16-bit or 32-bit or 64-bit or pointer). That's also why we do `xor eax, eax` at the end of `main()`, because that's like saying `return 0`.↵
↵
Note that this is grossly oversimplified and that the real calling convention is a lot more complex than this. But this should be good enough for now. The other gotcha that you should know for now is that some registers may be overwritten by the function that you call, while others might not. This is called caller vs callee saved registers; for now you can assume that none of your registers are safe and to always save them to memory.↵
↵
Example exercise↵
==================↵
↵
Write a program that reads two 32-bit signed integers from standard input, and tells tells the user "The sum of your two integers is <sum>." Use `scanf` and `printf` from the C stdio. You do not need to worry about overflow.↵
↵
<spoiler summary="How to allocate local variables, read this before attempting">↵
I told you earlier that with `push` and `pop`, the stack pointer is managed automatically. However this is not how compilers like to do it.↵
↵
They prefer to preallocate the stack: if you do `sub rsp, 16`, you now have 16 bytes of space to work with. Typically you'd address it with anything between `[rbp-1]` to `[rbp-16]`. (Remember that `rbp = rsp` initially, and now we subtracted 16 bytes from `rsp`, so we have 16 bytes of space between `rbp` and `rbp-16` to work with.)↵
↵
In the context of this problem, you could, for example, place `int var1` at `[rbp-4]` and `int var2` at `[rbp-8]`.↵
↵
Also, beware that `rsp` is required to be 16-byte aligned when you call something, or else you will get segfault. In practice, this means you should `sub rsp, a multiple of 16`.↵
</spoiler>↵
↵
<spoiler summary="Answer">↵
~~~~~↵
.intel_syntax noprefix↵
↵
.section .rodata↵
scan_format:↵
.ascii "%d %d"↵
print_format:↵
.ascii "The sum of your numbers is %d :yayy:\n"↵
↵
.section .text↵
.globl main↵
.type main, @function↵
main:↵
endbr64↵
push rbp↵
mov rbp, rsp↵
# Allocate space on the stack for two integers↵
# They only take 8 bytes, but rsp must be 16-byte aligned↵
sub rsp, 16↵
↵
# "%d %d"↵
lea rdi, scan_format[rip]↵
# &var1↵
lea rsi, [rbp-4]↵
# &var2↵
lea rdx, [rbp-8]↵
# eax must be zeroed for variadic functions↵
# The xor idiom is more popular than mov eax, 0↵
xor eax, eax↵
# scanf("%d %d", &var1, &var2)↵
call scanf↵
↵
# "The sum of your numbers is %d :yayy:\n"↵
lea rdi, print_format[rip]↵
# var1 + var2, in esi↵
# The DWORD PTR is optional here because it's obvious that we're loading 32 bits.↵
# However, in other cases it may be required.↵
mov esi, [rbp-4] # esi = var1↵
mov eax, [rbp-8] # eax = var2↵
add esi, eax # esi = var1 + var2↵
# eax must be zeroed for variadic functions↵
xor eax, eax↵
# printf("The sum of your numbers is %d :yayy:\n", var1 + var2)↵
call printf↵
↵
xor eax, eax↵
leave↵
ret↵
~~~~~↵
</spoiler>↵
↵
↵
↵
↵
In part 2, I'll probably cover more details about the calling convention (passing more arguments, structs, caller and callee saved registers) and talk about conditionals and branching. See you then!
↵
_This assumes you have a basic understanding of how pointers work in C_↵
↵
First program & compiling↵
==================↵
↵
Make a file named `hello.s`:↵
↵
~~~~~↵
.intel_syntax noprefix↵
↵
.section .rodata↵
mystring:↵
.ascii "Hello, World!\n"↵
mystring_end:↵
↵
.section .text↵
.globl main↵
.type main, @function↵
main:↵
endbr64↵
push rbp↵
mov rbp, rsp↵
↵
mov eax, 1 # write↵
mov rdi, 1 # stdout↵
lea rsi, mystring[rip]↵
mov rdx, mystring_end - mystring↵
syscall↵
↵
xor eax, eax↵
↵
leave↵
ret↵
~~~~~↵
↵
Compile and run with↵
↵
~~~~~↵
$ gcc -o hello hello.s↵
$ ./hello↵
~~~~~↵
↵
In assembly, you are writing CPU instructions, so you only have access to low-level things:↵
↵
- Registers: the main ones are `rax`, `rbx`, `rcx`, `rdx`, `rsi`, `rdi`, `rbp`, `rsp`, and `r8...r15`↵
- Memory (dereferencing pointers, writing to the stack, etc.)↵
- System calls↵
↵
The C stack↵
==================↵
↵
Your CPU only knows how to execute instructions, so how do we have functions be able to call other functions or themselves? The answer is the stack: each function has its own stack frame. For example, if `main` has its stack frame, and if it calls `printf`, then a new stack frame will be created for `printf`. Once `printf` is done executing, we will pop `printf`'s stack frame and go back where we left off to `main`.↵
↵
I'm now going to explain the actual nitty gritty of how this works; it may be a bit hard to follow along so if you're confused please let me know! I think it is fun to understand this though.↵
↵
Two registers are dedicated to managing the stack: `rbp`: **b**ase **p**ointer, and `rsp`: **s**tack **p**ointer. The base pointer points to the beginning/base of the stack frame, while the stack pointer points to the end. Also be aware that in x86, the stack grows _downward_, i.e., the `main` stack frame will be at a higher address in memory, and as more data is pushed onto the stack, the address _decreases_.↵
↵
Here's an example of how it works in practice.↵
↵
Suppose your code section looks like this (remember that code/instructions are also loaded in memory)↵
↵
~~~~~↵
address instruction↵
main:↵
...↵
5598 load pointer corresponding to format string into rdi↵
55a0 load integer into rsi↵
55a8 call printf↵
55b0 set exit code to 0↵
55b8 leave↵
55c0 ret↵
...↵
printf:↵
57d8 endbr64↵
57e0 push rbp↵
57e8 mov rbp, rsp↵
57f0 random instruction that does printing↵
57f8 another random instruction that does printing↵
5800 yet another random instruction that does printing↵
5808 leave↵
5810 ret↵
~~~~~↵
↵
So, we're currently on the stack frame of `main()` and we're about to execute the instruction `call printf` at `55a8`. After we're done executing `printf`, we need to execute the instruction at `55b0`. So, we push the address `55b0` onto the stack. This is called the **return address**, the CPU will jump to `55b0` after it's done executing `printf`.↵
↵
Now we jump to the address of `printf`, `57d8`. The first instruction is always `endbr64`, without getting too deep, it's a security feature; if you try to call a function and the first instruction is not `endbr64`, the CPU will segfault (if the security feature is enabled).↵
↵
Then, `push rbp` pushes the previous (`main`'s) base pointer to the stack. Then `mov rbp, rsp` makes `rbp` point to the beginning of the new stack frame, which is the current `rsp`. Note that the CPU automatically moves `rsp` to the end of the stack whenever anything is `push`ed or `pop`ed.↵
↵
The stack will look something like:↵
↵
~~~~~↵
address data↵
7ffe6590 <random data in the stack for main, like main's local variables>↵
7ffe6588 <random data in the stack for main, like main's local variables>↵
7ffe6580 <random data in the stack for main, like main's local variables>↵
7ffe6578 <return address telling us where to go back to in main()>↵
7ffe6570 <main's rbp value saved> # rbp now points to 7ffe6570↵
7ffe6568↵
7ffe6560↵
# rsp always points to the end of the stack, which is `7ffe6570` for now, but it may move down if `printf` has local variables, etc. that it wants to put on the stack.↵
~~~~~↵
↵
Eventually, `printf` will finish executing. The `leave` instruction is kind of like a macro — it will dereference `rbp` and set `rbp` to that value. `rbp` is a pointer to the beginning of the frame but it also points to `main`'s previous `rbp` value (see the figure above at `7ffe6570`). So this restores `rbp` back to its previous value when we go back to `main()`. It also automatically resets the stack pointer to the beginning of the frame, which effectively "deletes" everything in the current stack frame.↵
↵
Finally, `ret` will read the return address, which is now the last thing in the stack, and jump to the instruction we were previously at. The `rbp` and `rsp` registers are restored to the same places that they were before `printf` was called.↵
↵
Arithmetic operations↵
==================↵
↵
You can perform arithmetic operations on registers. For example, `add eax, ecx` performs `eax = eax + ecx`.↵
↵
Note that the letter you prefix a register with denotes its size: `rax` is the full 64-bit register, `eax` is the lower 32 bits of the register, `ax` is the lower 16 bits of the register, and `ah` is the upper byte of `ax`, and `al` is the lower byte of `ax`. The same thing applies for `rbx`, etc. For `r8` through `r15`, it's `r8` for full size, `r8d` for 32 bits, `r8w` for 16 bits, and `r8b` for 8 bits.↵
↵
So `add eax, ecx` performs 32-bit addition and `add rax, rcx` performs 64-bit addition. For addition and subtraction, the same instruction does both signed and unsigned addition/subtraction because of some magic in the representation of integers called 2's complement.↵
↵
For multiplication, it's a bit more complicated: `mul rxx` multiples `rax` by `rxx` and places the lower 64 bits in `rax` and upper 64 bits in `rdx`. If you do `mul exx`, it multiplies `eax` by `exx` and places the lower 32 bits in `eax` and upper 32 bits in `edx`.↵
↵
That's unsigned multiplication. Signed multiplication is `imul`.↵
↵
For division, it's also a little weird. `div rxx` takes the 128-bit value `rdx:rax` and divides it by `rxx`, places the quotient in `rax` and remainder in `rdx`. So if you want to do 64 bit division, you have to make sure to zero out `rdx`. Then `idiv rxx` is the same but for signed division. You may need to sign extend: `cqo` sign extends `rax` to `rdx:rax`, and `cdq` sign extends `eax` to `edx:eax`. (Sign extension is required if you're dealing with negative numbers.)↵
↵
The multiplication and division stuff is a bit complicated, but luckily we're in the age of AI so you don't have to memorize this.↵
↵
Moving data around↵
==================↵
↵
You've probably seen the move instruction: `mov rax, rdx` copies `rdx` into `rax` (and similar for 32-bit register views).↵
↵
To move between registers and memory, you can do something like `mov DWORD PTR [rbp-8], 572`. `[rbp-8]` means dereference the pointer `rbp-8`, so we're dereferencing the location 8 bytes below the base of the current stack frame. `DWORD PTR [rbp-8]` means we're interpreting it as a double word (32 bits). We also have `QWORD PTR` (64 bits), `WORD PTR` (16 bits), and `BYTE PTR`. So this writes the number `572` into the 32-bit integer located at `[rbp-8]`.↵
↵
Here are a few more examples. Can you tell what they mean?↵
↵
1. `mov eax, DWORD PTR [rbp-20]`↵
2. `mov QWORD PTR [rbp-8], rbx`↵
↵
<spoiler summary="Spoiler">↵
1. Load 32 bits at `[rbp-20]` in memory into `eax`.↵
2. Store the 64-bit `rbx` at `[rbp-8]` in memory.↵
</spoiler>↵
↵
If you look at assembly generated by GCC, you'll see a lot of moves relative to `rbp`. That's because we like to reference variables by their location relative to the base of the stack frame. For example, if you declare `int a, b, c;` in a function, GCC may decide to place `a` at `[rbp-4]`, `b` at `[rbp-8]`, and `c` at `[rbp-12]`.↵
↵
We also have `lea` for **load effective address**. It's typically useful for pointer arithmetic (you can also just use `add`/`sub` but `lea` is typically more idiomatic and faster).↵
↵
For example,↵
↵
~~~~~↵
# This loads `rbx + rax` as an address into `rdi`. It's equivalent to `rdi = rbx + rax` so you can technically replace it with `add` instructions. In practice this could mean that `rbx` was a pointer to an array of chars and `rax` is the index, so this is `&rbx[rax]`.↵
# Note that we have brackets here but the memory is not actually dereferenced.↵
lea rdi, [rbx + rax]↵
↵
# If `rbx` is a pointer to an array of 4-byte ints, then this is like `&rbx[rax]`.↵
# Note that you can't just put arbitrary arithmetic here, lea only allows `[base + size*index + offset]`. And size can only be 1, 2, 4, 8.↵
lea rdi, [rbx + 4*rax]↵
↵
# If `rbx` is a pointer to 8-byte structs, and `field` is located at a 3-byte offset in the struct, then this is like `&rbx[rax].field`.↵
lea rdi, [rbx + 8*rax + 3]↵
~~~~~↵
↵
Sections↵
==================↵
↵
There are `.section` directives. The sections are:↵
↵
- `.text`: code↵
- `.data`: global variables, that can be pre-initialized to a certain value↵
- `.bss`: global variables, that are automatically zero-initialized↵
- `.rodata`: read-only constants, such as literal strings in C↵
↵
### Global variables↵
↵
This in C:↵
↵
~~~~~↵
int32_t y = 1000;↵
int64_t z = 67;↵
char s[] = "forcescode";↵
~~~~~↵
↵
Is this in assembly:↵
↵
~~~~~↵
.section .data↵
↵
.globl y↵
y:↵
# Also .byte, .short↵
.long 1000↵
↵
.globl z↵
.align 8 # Unaligned data will not cause errors in modern x86_64, but it is slower.↵
z:↵
.quad 67↵
↵
.global s↵
s:↵
# .string or .asciz null-terminate the string, .ascii does not↵
.string "forcescode"↵
~~~~~↵
↵
Note that `.globl` is a directive for it to be visible to outside files or not. It's like static vs non-static in C.↵
↵
For bss, you just reserve space:↵
↵
~~~~~↵
.section .bss↵
↵
.globl myarray↵
myarray:↵
.zero 24000 # 24,000 bytes↵
~~~~~↵
↵
### String literals↵
↵
Typically, you'll put these in rodata:↵
↵
~~~~~↵
.section .rodata↵
↵
mystring:↵
.string "codeforces"↵
~~~~~↵
↵
Also, typically when referencing data or bss values, you need **instruction pointer relative addressing**. Because your code needs to be position-independent, you can't have a fixed address to reference the global variable, you need it relative to `rip`, the instruction pointer register.↵
↵
~~~~~↵
# Equivalent↵
lea rdi, mystring[rip]↵
lea rdi, [rip + mystring]↵
~~~~~↵
↵
Both of these reference `mystring`, but as an offset relative to `rip`. At link time, the offset of `mystring` relative to `rip` is calculated, and then the code will correctly reference `mystring`.↵
↵
Basic calling convention↵
==================↵
↵
I'll probably go in more detail on this in part 2. For now, here's the basics of how to call other functions correctly.↵
↵
You pass the first **6** arguments in registers: `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`. The classic way to remember it is " **Di**ana's **si**lky **d**ress **c**osts **89**".↵
↵
Additionally, for variable argument functions like `printf` or `scanf`, you also need to zero out `eax`. (I'll explain why later.)↵
↵
For example, if you wanted to `printf("The answer is: %d\n", answer)`, you would↵
↵
~~~~~↵
lea rdi, location_of_string[rip] # Assuming the string literal is in .rodata↵
mov esi, [pointer to variable `answer`]↵
xor eax, eax # "Better" way to zero out a register than `mov eax, 0`↵
call printf↵
~~~~~↵
↵
Then, the function's return value is given in `rax`/`eax`/`ax`/`al` (depending on if the return value is a 16-bit or 32-bit or 64-bit or pointer). That's also why we do `xor eax, eax` at the end of `main()`, because that's like saying `return 0`.↵
↵
Note that this is grossly oversimplified and that the real calling convention is a lot more complex than this. But this should be good enough for now. The other gotcha that you should know for now is that some registers may be overwritten by the function that you call, while others might not. This is called caller vs callee saved registers; for now you can assume that none of your registers are safe and to always save them to memory.↵
↵
Example exercise↵
==================↵
↵
Write a program that reads two 32-bit signed integers from standard input, and tells tells the user "The sum of your two integers is <sum>." Use `scanf` and `printf` from the C stdio. You do not need to worry about overflow.↵
↵
<spoiler summary="How to allocate local variables, read this before attempting">↵
I told you earlier that with `push` and `pop`, the stack pointer is managed automatically. However this is not how compilers like to do it.↵
↵
They prefer to preallocate the stack: if you do `sub rsp, 16`, you now have 16 bytes of space to work with. Typically you'd address it with anything between `[rbp-1]` to `[rbp-16]`. (Remember that `rbp = rsp` initially, and now we subtracted 16 bytes from `rsp`, so we have 16 bytes of space between `rbp` and `rbp-16` to work with.)↵
↵
In the context of this problem, you could, for example, place `int var1` at `[rbp-4]` and `int var2` at `[rbp-8]`.↵
↵
Also, beware that `rsp` is required to be 16-byte aligned when you call something, or else you will get segfault. In practice, this means you should `sub rsp, a multiple of 16`.↵
</spoiler>↵
↵
<spoiler summary="Answer">↵
~~~~~↵
.intel_syntax noprefix↵
↵
.section .rodata↵
scan_format:↵
.ascii "%d %d"↵
print_format:↵
.ascii "The sum of your numbers is %d :yayy:\n"↵
↵
.section .text↵
.globl main↵
.type main, @function↵
main:↵
endbr64↵
push rbp↵
mov rbp, rsp↵
# Allocate space on the stack for two integers↵
# They only take 8 bytes, but rsp must be 16-byte aligned↵
sub rsp, 16↵
↵
# "%d %d"↵
lea rdi, scan_format[rip]↵
# &var1↵
lea rsi, [rbp-4]↵
# &var2↵
lea rdx, [rbp-8]↵
# eax must be zeroed for variadic functions↵
# The xor idiom is more popular than mov eax, 0↵
xor eax, eax↵
# scanf("%d %d", &var1, &var2)↵
call scanf↵
↵
# "The sum of your numbers is %d :yayy:\n"↵
lea rdi, print_format[rip]↵
# var1 + var2, in esi↵
# The DWORD PTR is optional here because it's obvious that we're loading 32 bits.↵
# However, in other cases it may be required.↵
mov esi, [rbp-4] # esi = var1↵
mov eax, [rbp-8] # eax = var2↵
add esi, eax # esi = var1 + var2↵
# eax must be zeroed for variadic functions↵
xor eax, eax↵
# printf("The sum of your numbers is %d :yayy:\n", var1 + var2)↵
call printf↵
↵
xor eax, eax↵
leave↵
ret↵
~~~~~↵
</spoiler>↵
↵
↵
↵
↵
In part 2, I'll probably cover more details about the calling convention (passing more arguments, structs, caller and callee saved registers) and talk about conditionals and branching. See you then!



