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, andr8...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: base pointer, and rsp: stack pointer. 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 pushed or poped.
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?
mov eax, DWORD PTR [rbp-20]mov QWORD PTR [rbp-8], rbx
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 " Diana's silky dress costs 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 ." Use scanf and printf from the C stdio. You do not need to worry about overflow.
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!









You could just have posted a link to a textbook, and provided better content in fewer words. This reads like an uninspired and rambling information dump.
Well, I tried my best. I don’t think textbooks cover this stuff very well, I learned it all mostly from talking to my professor in one of my systems courses.
and this is just misinformation without the relevant context, the ABI only requires rsp to be 16-byte aligned just before calling something
Fixed
You could just have posted a link to a textbook, and provided better content in more words. This reads like an uninspired and rambling complaint.
Auto comment: topic has been updated by greateric (previous revision, new revision, compare).
bro codeforces isn't your university notebook
Upvoted. All who downvote this blog are idiots. x86-64 asm and ARM asm are both nice(ok, x86-64 version is a bit more readable). Waiting first contest in asm only(you can do inline assembly right?)
lol in our architecture course it was exclusively arm and I did not like it
I read this: this and this. Yes, I know it's "useless," but at least it's fun, and no one's stopping me from doing it. In addition, at the university I had a course purely(not operation systems/computer architecture) on x86-64 asm, which was also cool.
ldr and str just feel weird to me. mov supremacy
intrinsics did NOT exist for this bro :sob:
Not a bad tutorial. Here are my 2 cents: I think you might want to mention, that you're using the system V ABI calling convention+syscalls, so the code wouldn't exhibit the same behavior on e.g. Windows, which has a diff ABI. Are you planning to dive into optimization in asm?
Ah shoot I did not realize. Probably not too much advanced stuff because I low key don't know it :sob: I just learned the basics a while ago and thought it was funny
deleted
Assembly bros be like
At least I’m not making you build the transistors yourself
Assembly has got its own aura ig!
cool mnemonic!
It’s fairly standard lol