Add x86-assembly

This commit is contained in:
dwrz
2026-10-27 12:41:06 +00:00
parent 42c2105b16
commit dcacadffc0
11 changed files with 632 additions and 0 deletions
+391
View File
@@ -0,0 +1,391 @@
* 1. x86 Assembly
- David Wen Riccardi-Zhu
- Senior Software Engineer @ Good Uncle
- dwrz@dwrz.net
Slides and code: https://github.com/dwrz/talks/x86-assembly.
* 2. Overview
1. Follow a trail of questions... into the depths of the machine.
2. Peel away some abstractions on the way.
3. Gloss over a bunch of stuff (we have an hour, not a semester).
4. Surface (hopefully) with a better understanding of how computers work.
* 3. What does this program do?
#+begin_src bash :results raw
node -e "console.log('Hello, World');"
#+end_src
* 4. How does this program /work/?
#+begin_src bash :results raw
node -e "console.log('Hello, World');"
#+end_src
How does "Hello World" get onto the terminal?
* 5. What does this program do?
#+begin_src bash
node -e ""
#+end_src
Does it do /anything/?
* 6. It does /something/.
We can tell, because it takes a few milliseconds to execute.
#+begin_src bash :results raw
time node -e "console.log('Hello, World');"
#+end_src
#+begin_src bash :results raw
time node -e ""
#+end_src
* 7. It /definitely/ does /something/.
We can tell, because it's making system calls.
#+begin_src bash :results raw
strace node -e ""
#+end_src
Lot of noise for a program that does "nothing"...
#+begin_src bash :results raw
strace node -e "console.log('Hello, World');"
#+end_src
* 8. JavaScript
We've actually started with something difficult.
JavaScript is an interpreted language.
#+begin_src js
const myProgram = '0 + 1'.replace('+', '-');
const result = eval(myProgram);
console.log(result); // -1
#+end_src
#+RESULTS:
* 9. C
Let's use a simpler example.
C is compiled.
We create our own program, rather than use a program that interprets strings.
#+begin_src C :results raw
#include <stdio.h>
int main(void) {
printf("Hello, World\n");
}
#+end_src
* 10. Compile and Execute
#+begin_src bash
gcc hello-world.c -o hello-world
#+end_src
#+begin_src bash
./hello-world
#+end_src
#+begin_src bash
strace ./hello-world
#+end_src
* 11. write
This seems familiar:
#+begin_src C
write(1, "Hello, World\n", 13Hello, World
) = 13
#+end_src
It's in our node program, too:
#+begin_src bash
strace node -e "console.log('Hello, World');" 2>&1 | grep "Hello"
#+end_src
/What is this?/
What is ~write~?
What is 1?
What is 13?
* 12. System Calls
~strace~ shows us system calls. ~write~ is a system call.
What is a system call?
#+begin_src bash
man syscalls
#+end_src
#+begin_quote
The system call is the fundamental interface between an application and the Linux kernel.
#+end_quote
* 13. write
#+begin_src bash
man 2 write
#+end_src
#+begin_src C
write(int fd, const void *buf, size_t count);
#+end_src
#+begin_quote
write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.
#+end_quote
* 14. write
#+begin_src text
write(1, "Hello, World\n", 13Hello, World
) = 13
#+end_src
File Descriptor 1 = Standard Out (inherited from terminal process)
Hello World = Buffer
Count = 13 bytes
|---+---+---+---+---+---+---+---+---+---+---+---+----|
| H | e | l | l | o | , | | W | o | r | l | d | \n |
|---+---+---+---+---+---+---+---+---+---+---+---+----|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 |
|---+---+---+---+---+---+---+---+---+---+---+---+----|
* 15. Back to Nothing
#+begin_src C
int main(void) {}
#+end_src
#+begin_src bash
gcc exit.c -o exit
#+end_src
#+begin_src bash
strace ./exit
#+end_src
* 16. Exit Code
In C, the return type prefixes the function.
~main~ returns an ~int~; the default is zero (indicating no error).
#+begin_src C
int main(void) {
return 1;
}
#+end_src
#+begin_src C
exit_group(1)
#+end_src
* 17. exit_group
#+begin_src bash
man 2 exit_group
#+end_src
#+begin_src C
void exit_group(int status);
#+end_src
* 18. Exit
[[file:src/exit/exit.s:]]
* 19. Assemble, Link, Execute, Trace
#+begin_src bash
as exit.s -o exit.o
ld exit.o -o exit
./exit
strace ./exit
#+end_src
* 20. x86 Assembly
- Human readable form of machine code.
- 1-to-1 mapping between one assembly instruction and one CPU instruction.
- Hardware specific: e.g., x86 Assembly differs from ARM Assembly.
- Often OS specific --> Linux System Calls != BSD, Mac, Windows system calls.
- Different syntax formats: ATT, Intel.
- What instructions? Need to consult hardware manual.
- [[https://software.intel.com/content/www/us/en/develop/articles/intel-sdm.html][Intel x86 Developer Manual]] is ~5,000 pages long, plus errata.
* 21. Use Cases
- Low-level programming (micro-controllers, operating systems)
- Resource Constrained Hardware
- [[https://github.com/chrislgarry/Apollo-11][Apollo 11 Guidance Computer Assembly]]
- [[https://github.com/pret/pokered][Pokemon Red/Blue Assembly]]
- Performance
- Control
- Reverse Engineering
* 22. Instructions
- Describe an operation the CPU should perform, e.g.:
- Move data in and out of registers
- Modify register contents
- Modify stack
- Control program flow
- Represented by numbers (opcodes).
* 23. Instruction Cycle
- On every tick of its internal clock, the CPU:
- *Fetches* the next instruction.
- *Decodes* it (what operation, on what operands).
- *Executes* the instruction.
- Increments the instruction pointer.
* 24. Registers
- Storage on the CPU (fastest storage).
- Act as a scratchpad -- temporary variables.
- General Purpose Registers
- RAX, RBX, RCX
- RSP, RBP (stack pointer, stack frame pointer)
- Special Purpose Registers
- RIP (Instruction Pointer)
- RFLAGS (negative, zero, etc.)
- It's possible to use just a portion of the register.
#+begin_src text
|__64__|__56__|__48__|__40__|__32__|__24__|__16__|__8___|
|__________________________RAX__________________________|
|xxxxxxxxxxxxxxxxxxxxxxxxxxx|____________EAX____________|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|_____AX______|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|__AH__|__AL__|
#+end_src
* 25. Exit++
[[file:src/math/math.s]]
* 26. Sections
What happens when we run a program? A few things...
One of them: the kernel loads the executable into memory.
Assembly sections refer to executable's memory layout:
|-----------|
| TEXT | --> Code (instructions)
| RODATA | --> const str = "Hello, World";
| DATA | --> var str = "Hello, World";
| BSS | --> var str;
| ↓ HEAP ↓ |
| |
| ↑ STACK ↑ |
|-----------|
* 27. Hello World
[[file:src/hello-world/hello-world.s]]
* 28. Control Flow
Programs are either sequential, looping, or branching.
- CPU sets FLAGS register after instruction: e.g., result is zero, negative.
- Jump to code based on the state of FLAGS.
- Jump changes RIP (instruction pointer).
[[file:src/control-flow/control-flow.s]]
* 29. Stack
RSP register points to the top of the stack.
RBP register (typically) points to the (current) base of the stack.
Together, they form a stack frame.
Instructions:
- ~push~ :: decrements RSP, moves bytes onto stack.
- ~pop~ :: increments RSP, moves stack bytes into register.
[[file:src/stack/stack.s]]
* 30. Functions
Why do we use functions? Same reasons apply in Assembly:
- Reuse
- Organization
- Abstraction
- Splitting work
Problems:
- How to pass arguments?
- Registers -- which ones?
- Stack -- what order?
- Whose job is it to preserve or clean up registers? Caller? Callee?
- E.g., caller saves a value in %rbx to use after function returns.
- Callee uses %rbx and overwrites that value.
- How to pass return value(s)?
* 31. Convention
Which side of the street should we drive on?
Either way works, both are used in practice.
What matters is agreement on an approach.
System V AMD64 ABI is calling convention for Unix x64 systems:
- Some registers must be saved by the caller, so callee can use them.
- Some registers must be saved by callee, if the plan to use them later.
- Some registers used to pass arguments.
- Stack used to pass extra or large arguments.
- RAX and RDX are used for return values.
* 32. Stack Arguments
[[file:src/func/func.s]]
Each row is 8 bytes (64 bits).
|----------------+-----------+----------------|
| Address | Data | Stack Pointers |
|----------------+-----------+----------------|
| 0x7fffffffe8f8 | | |
| 0x7fffffffe900 | 0x0 (rbp) | |
| 0x7fffffffe908 | 0x401002 | |
| 0x7fffffffe910 | 3 | ←rsp |
|----------------+-----------+----------------|
←rbp
* 33. Safety and Security
[[file:src/safety/safety.s]]
* 34. Review
Where we started:
#+begin_src bash
node -e ""
#+end_src
- CPU processes instructions
- Uses registers and memory (stack)
- Control flow with jump instruction and flags register
- Functions
- System Calls
- Comparison with Compiled and Interpreted Languages
- Tradeoffs
* 35. Conclusion
- Appreciation for higher level, and work done to get us here.
- Insight into how computers work.
- A platform to better understand things like functions, closures, APIs, pass by reference and pass by value, performance.
* 36. References / Further Reading
- Davy Wybiral, [[https://www.youtube.com/playlist?list=PLmxT2pVYo5LB5EzTPZGfFN0c2GDiSXgQe][Intro to x86 Assembly Language]]
- Jennifer Rexford, [[https://www.cs.princeton.edu/courses/archive/fall05/cos217/][Princeton COS 217: Introduction to Programming Systems]]
- [[https://en.wikipedia.org/wiki/Objdump][objdump]]
- [[https://en.wikipedia.org/wiki/Structured_program_theorem][Structured Program Theorem]]
- [[https://www.gnu.org/software/gdb/][GDB]]
- [[https://www.youtube.com/watch?v=tpIctyqH29Q&list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo][Crash Course: Computer Science]]
@@ -0,0 +1,23 @@
.data
stop:
.int 3
.text
.global _start
_start:
mov $0, %rcx
loop:
cmp stop, %rcx
# If equal, change RIP to exit.
je exit
inc %rcx
# Unconditionally change RIP to loop.
jmp loop
exit:
# Try this: comment out the following three instructions.
# What happens if you assemble and link? Why?
# Think about RIP and the Fetch-Decode-Execute cycle.
mov $231, %rax
mov %rcx, %rdi # Exit code -- let's see the value of %rcx.
syscall
+3
View File
@@ -0,0 +1,3 @@
int main(void) {
// return 1;
}
+12
View File
@@ -0,0 +1,12 @@
.text
.global _start
_start:
# Store system call 1 in the EAX register.
mov $1, %eax
# Store the exit code 0 in the EBX register.
mov $0, %ebx
# Call interrupt handler 0x80 (128) -- on Linux, this is the kernel.
int $0x80
+31
View File
@@ -0,0 +1,31 @@
.text
.global _start
_start:
# Store 3 on the stack.
push $3
# call stores return address on the stack.
call square
# Use the return value, in %rax, as our exit status number.
mov %rax, %rdi
mov $231, %rax
syscall
# square expects an integer on the stack, 16 bytes down from RBP.
# The squared value is returned on %rax.
square:
# Function Epilogue --> {
push %rbp
mov %rsp, %rbp
# Retrieve the parameter from the stack.
# 8 bytes down from RSP is the return address from square.
# 8 bytes down from that is the parameter to multiple.
mov 16(%rbp), %rax
mul %rax
# Function Prologue --> }
mov %rbp, %rsp
pop %rbp
# ret pops and return to address on stack.
ret
@@ -0,0 +1,5 @@
#include <stdio.h>
int main(void) {
printf("Hello, World\n");
}
@@ -0,0 +1,17 @@
.data
str:
.ascii "Hello, World\n"
.text
.global _start
_start:
mov $1, %rax # 64-bit system call number for sys_write.
mov $1, %rdi # stdout.
mov $str, %rsi # Address of the buffer.
mov $13, %rdx # Number of bytes to write.
syscall
mov $231, %rax # 64-bit system call number for exit_group.
mov $0, %rdi # Exit code.
syscall
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"fmt"
"strings"
"time"
)
func main() {
now := time.Now()
for i := 0; i < 1000; i++ {
fmt.Println(i)
}
fmt.Printf("took %v\n", time.Since(now))
now = time.Now()
var str strings.Builder
for i := 0; i < 1000; i++ {
str.WriteString(fmt.Sprintf("%d\n", i))
}
fmt.Println(str.String())
fmt.Printf("took %v\n", time.Since(now))
}
+32
View File
@@ -0,0 +1,32 @@
.text
.global _start
_start:
mov $0, %rax # rax = 0
inc %rax # rax++
dec %rax # rax--
add $7, %rax # rax += 7
sub $5, %rax # rax -= 5
# At this point, rax = 2.
# Multiplication cannot use immediate addressing.
# So we store the multiplier in %rbx.
# mul assumes the multiplicand is in %rax.
# The product is also stored in %rax.
mov $12, %rbx
mul %rbx # rax *= 12
# rax = 24.
# Division also cannot use immediate addressing.
# Store the divisor in %rbx.
# div assumes the dividend is in %rax.
# The quotient is also stored in %rax.
mov $3, %rbx
div %rbx
# rax = 8
# Let's use this as our exit code.
# We're using 64-bits now -- the exit code should be in %rdi.
mov %rax, %rdi
mov $231, %rax
syscall
+47
View File
@@ -0,0 +1,47 @@
.data
romeo:
.ascii "What's in a name? That which we call a rose,\n"
juliet:
.ascii "By any other name would smell as sweet.\n"
.ascii "\n"
stop:
# Try changing this number. What happens? Why?
# Hint: try 0, 1, 11, 12, 21, 22.
.int 11
.text
.global _start
_start:
# Print the string before it's messed up.
mov $1, %rax # sys_write.
mov $1, %rdi # stdout
mov $romeo, %rsi # address
mov $86, %rdx # count bytes
syscall
# Setup.
lea romeo, %rax # Load the starting address of string into %rax.
mov $0xb98c9ff0, %ebx # 🌹 = b98c9ff0 (4 bytes)
mov $0, %rcx # Set the intial counter.
# Overwrite the strings with 🌹.
# Increment the address in %rax by four bytes on each iteration.
loop:
mov %ebx, (%rax) # Copy
inc %rcx # increment our counter
add $4, %rax # Try changing this to sub. What happens? Why?
cmp stop, %rcx
jne loop
# Print the string after it's been messed up.
# NB: nothing stopped us from overwriting past the "romeo" string.
mov $1, %rax # sys_write
mov $1, %rdi # stdout
mov $romeo, %rsi # address
mov $85, %rdx # count bytes
syscall
mov $231, %rax # 64-bit system call number for exit_group.
mov $0, %rdi # Exit code.
syscall # Call the kernel.
+48
View File
@@ -0,0 +1,48 @@
.text
.global _start
_start:
# Push some 64-bit integers onto the stack.
push $3
push $2
push $1
# Let's print "Hey\n", using the stack.
# man ascii
# |---------+----+-----+-----+----|
# | ASCII | H | e | y | \n |
# |---------+----+-----+-----+----|
# | Decimal | 72 | 101 | 121 | 10 |
# |---------+----+-----+-----+----|
# | Hex | 48 | 65 | 79 | 0A |
# |---------+----+-----+-----+----|
# x86 is little-endian: it stores the least significant byte
# of a multi-byte value at the lowest memory address.
# Since the write system call reads memory from low to high address,
# we put the first character ('H' = 0x48) in the least significant byte.
# <-- high low -->
# \n y e H
push $0x000000000A796548
# %rsp points to the top of the stack (the lowest address of our push,
# where 'H' is stored).
# We can pass this address to the kernel.
# write will start at 'H', and work up to '\n'.
mov $1, %rax # write
mov $1, %rdi # stdout
lea (%rsp), %rsi # address of buffer
# Try changing the number of bytes to 9, 17, 25.
# Then call the program with strace.
# You'll be able to see the previous numbers we pushed onto the stack.
mov $4, %rdx # number of bytes
syscall
# Remember, the stack is LIFO.
pop %rdi # pop the "Hey\n" string
pop %rdi # pop 1
pop %rdi # pop 2
# We'll use the last popped value in %rdi as our exit code.
mov $231, %rax
syscall