NOTE
Assembly Language
Historical study notes on x86-64 AT&T assembly, registers, addressing, function calls, and C/Go disassembly.
This is a historical learning note and may contain outdated or incomplete understanding.
1. What Is Assembly Language?
A computer directly executes machine instructions, but machine language is difficult for humans to read. Assembly language uses mnemonics to represent machine instructions and usually maps directly to them.
2. Registers
2.1. Register Names
This note uses AT&T syntax, where register names use a % prefix.
2.2. Categories
%rspis the stack-pointer register and usually points to the top of the stack. Stack pop and push operations are implemented by changing the value of%rsp, that is, by moving the stack pointer.%rbpis the stack-frame pointer and is used to identify the start of the current stack frame.- Under the System V AMD64 ABI, the first six integer or pointer arguments use
%rdi,%rsi,%rdx,%rcx,%r8, and%r9; the exact registers depend on the platform ABI. %raxis usually used to store the return result of a function call and is also used in multiplication and division instructions.
2.3. caller save / callee save
A function call needs to save register values so they can be restored after returning from the child function.
- caller save: the caller saves register values on the stack.
- callee save: the callee saves register values on the stack.
3. Addressing Modes
- The format of register-indirect addressing is
offset(%register).
4. Assembly Instruction Format
Using AT&T-format assembly instructions as an example:
opcode [operand]- Opcode: indicates what operation the CPU should execute.
- Operand: the object being operated on.
- The number of operands is generally 0, 1, or 2.
- For instructions with two operands, the first is the source operand and the second is the destination operand.
- An immediate operand needs a
$prefix.- In
mov $0x1 %rdi, the first operand is neither a register nor a memory address, but a constant written directly in the instruction.
- In
5. Common Instructions
5.1. mov
- Copies the source operand to the destination operand.
- Format
mov source_operand,destination_operand
- Example
mov %rsp,%rbp # direct addressing: copy rsp to rbp, equivalent to rbp = rsp
mov -0x8(%rbp),%edx # source uses indirect addressing and destination uses direct addressing; read 4 bytes from memory into edx
mov %rsi,-0x8(%rbp) # source uses direct addressing and destination uses indirect addressing; write the 8-byte value in rsi to memory
5.2. add / sub
- Addition and subtraction instructions.
- Format
add source_operand,destination_operand
sub source_operand,destination_operand
- Example
sub $0x350,%rsp # source is an immediate operand and destination uses direct addressing. rsp = rsp - 0x350
add %rdx,%rax # direct addressing. rax = rax + rdx
addl $0x1,-0x8(%rbp) # source is an immediate operand and destination uses indirect addressing. Increment the value in memory by 1 (the suffix l in addl indicates operating on memory
5.3. call / ret
call: saves the return address and jumps to the child function for execution.
On x86-64, call pushes the address of the instruction immediately after the call onto the stack.

ret: pops the return address that was pushed by call into rip, thereby returning from the called function to the calling function to continue execution.
- Format
call target_address
ret
- Example
# 2. Calling-function fragment
0x0000000000400559: callq 0x400526 <sum>
0x000000000040055e: mov %eax,-0x4(%rbp)
--------------------------------------------------
# 3. Called-function fragment
0x0000000000400526: push %rbp
......
0x000000000040053f: retq
5.4. Instructions Beginning with j, Such as jmp / je / jle / jg / jge
- These are jump instructions. The opcode is followed directly by the address to jump to or a register containing the address. These instructions correspond to statements such as
gotoandifin high-level programming languages. - Example
jmp 0x4005f2
jle 0x4005ee
jl 0x4005b8
5.5. push / pop
- Instructions dedicated to pushing to and popping from the function call stack. Both instructions automatically modify the
rspregister. - Format
push source_operand
pop destination_operand
5.6. leave

leavehas no operands. It is generally placed at the end of a function beforeretand is used to adjustrspandrbp. This instruction is equivalent to the following two instructions:
mov %rbp,%rsp # make %rsp and %rbp point to the same location, i.e. the start of the child stack frame
pop %rbp # assign the saved parent-frame %rbp value on the stack to %rbp, and move %rsp upward to point to the end of the parent stack frame
6. Examples
6.1. Function Calls
How does the CPU jump from the caller to the called function for execution?
How are parameters passed from the caller to the called function?
How is memory for local variables of a function allocated on the stack?
How is a return value returned from the called function to the caller?
What cleanup work needs to be done after the function finishes executing?
6.2. C Function
How parameters are passed depends on the ABI / calling convention. The C disassembly below comes from a Windows x64 environment, where integer arguments are passed in registers first; call pushes the return address, and the function prologue then saves required registers and allocates stack space.
6.2.1. hello world
#include <stdio.h>
// sum parameters a and b
int sum(int a, int b)
{
int s=a+b;
return s;
}
// main function: program entry
int main(int argc, char*argv[])
{
int n=sum(1, 2); // call sum to calculate the sum
printf("n: %d\n", n); // output n on the screen
return 0;
}
6.2.2. Compile with gcc
gcc call.c -o call.exe
6.2.3. Debug with gdb
gdb .\call.exe
6.2.4. Disassemble main
(gdb) disass main
Dump of assembler code for function main:
// function call
0x00000001004010a2 <+0>: push %rbp # save the old frame pointer, equivalent to creating a new stack frame
0x00000001004010a3 <+1>: mov %rsp,%rbp # make %rbp point to the starting position of the new stack frame
0x00000001004010a6 <+4>: sub $0x30,%rsp # reserve some space in the new stack frame for the subroutine, referenced as (%rsp+K) or (%rbp-K)
0x00000001004010aa <+8>: mov %ecx,0x10(%rbp)
0x00000001004010ad <+11>: mov %rdx,0x18(%rbp)
0x00000001004010b1 <+15>: callq 0x100401100 <__main>
0x00000001004010b6 <+20>: mov $0x2,%edx
0x00000001004010bb <+25>: mov $0x1,%ecx
0x00000001004010c0 <+30>: callq 0x100401080 <sum>
0x00000001004010c5 <+35>: mov %eax,-0x4(%rbp)
0x00000001004010c8 <+38>: mov -0x4(%rbp),%eax
0x00000001004010cb <+41>: mov %eax,%edx
// function return
0x00000001004010cd <+43>: lea 0x1f2c(%rip),%rcx # 0x100403000
0x00000001004010d4 <+50>: callq 0x100401110 <printf>
0x00000001004010d9 <+55>: mov $0x0,%eax
0x00000001004010de <+60>: add $0x30,%rsp
0x00000001004010e2 <+64>: pop %rbp
0x00000001004010e3 <+65>: retq
0x00000001004010e4 <+66>: nop
0x00000001004010e5 <+67>: nop
0x00000001004010e6 <+68>: nop
0x00000001004010e7 <+69>: nop
0x00000001004010e8 <+70>: nop
0x00000001004010e9 <+71>: nop
0x00000001004010ea <+72>: nop
0x00000001004010eb <+73>: nop
0x00000001004010ec <+74>: nop
0x00000001004010ed <+75>: nop
0x00000001004010ee <+76>: nop
0x00000001004010ef <+77>: nop
End of assembler dump.
- The format is as follows:
- instruction address
- byte offset of the instruction relative to the start address of the current function
- instruction
6.2.5. Set a Breakpoint on the First Instruction of main and Run
(gdb) b *0x00000001004010a2 // set breakpoint
Breakpoint 1 at 0x1004010a2
(gdb) r // run
Starting program: /c/Users/user/code/test/call.exe
[New Thread 12648.0x738]
[New Thread 12648.0x421c]
[New Thread 12648.0x20b0]
[New Thread 12648.0x123c]
[New Thread 12648.0x254]
[New Thread 12648.0x14d4]
Thread 1 "call" hit Breakpoint 1, 0x00000001004010a2 in main () // stopped at the first instruction
6.2.6. Disassemble main Again
(gdb) disass
Dump of assembler code for function main:
=> 0x00000001004010a2 <+0>: push %rbp
0x00000001004010a3 <+1>: mov %rsp,%rbp
0x00000001004010a6 <+4>: sub $0x30,%rsp
//...
- The first three instructions are called the function prologue. They are used to save the caller’s
rbpregister and allocate stack space for the current function. =>indicates the next instruction the CPU will execute.
6.2.7. View All Register Values
(gdb) i r
rax 0x100000000 4294967296
rbx 0x180239780 6444783488
rcx 0x1 1
rdx 0xffffcc60 4294954080
rsi 0x0 0
rdi 0x20 32
rbp 0xffffcce0 0xffffcce0
rsp 0xffffcc18 0xffffcc18
r8 0x80003a790 34359977872
r9 0x0 0
r10 0x180239f20 6444785440
r11 0x0 0
r12 0xffffcc60 4294954080
r13 0xffffcc70 4294954096
r14 0xffffcc70 4294954096
r15 0xffffcc97 4294954135
rip 0x1004010a2 0x1004010a2 <main>
--Type <RET> for more, q to quit, c to continue without paging--
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x53 83
gs 0x2b 43
- The value of
ripis0x1004010a2, pointing to the next instruction to execute. - The value of
rspis0xffffcc18, pointing to the top of the stack. - The value of
rbpis0xffffcce0, pointing to the start address of the current stack frame.
6.3. Go Function
6.3.1. hello world
package main
// calculate the sum of squares of a and b
func sum(a, b int) int {
a2 := a * a
b2 := b * b
c := a2 + b2
return c
}
func main() {
sum(1, 2)
}
- go build
go build -gcflags "-N -l" call2.go
6.3.2. Debug with gdb
gdb .\call2.exe
6.3.3. Set a Breakpoint on main
(gdb) break main.main
Breakpoint 1 at 0x455a96: file C:/Users/user/code/tmp/call2.go, line 12.
6.3.4. Run
(gdb) r
Starting program: C:\Users\user\code\tmp\call2.exe
[New Thread 4416.0x27f4]
[New Thread 4416.0x2dc0]
[New Thread 4416.0x2cac]
[New Thread 4416.0x1bac]
[New Thread 4416.0x6c8]
[New Thread 4416.0x208]
[New Thread 4416.0x18e8]
[New Thread 4416.0xb68]
Thread 1 hit Breakpoint 1, main.main () at C:/Users/user/code/tmp/call2.go:12
12 func main() {
6.3.5. Disassembly
(gdb) disass main.main
Dump of assembler code for function main.main:
0x0000000000455a80 <+0>: mov %gs:0x28,%rcx
0x0000000000455a89 <+9>: mov 0x0(%rcx),%rcx
0x0000000000455a90 <+16>: cmp 0x10(%rcx),%rsp
0x0000000000455a94 <+20>: jbe 0x455ac4 <main.main+68>
=> 0x0000000000455a96 <+22>: sub $0x20,%rsp # reserve 32 bytes of stack space for main
0x0000000000455a9a <+26>: mov %rbp,0x18(%rsp) # save the caller's rbp register
0x0000000000455a9f <+31>: lea 0x18(%rsp),%rbp # adjust rbp to point to the start address of main's stack frame
0x0000000000455aa4 <+36>: movq $0x1,(%rsp) # push the first parameter (1) of sum onto the stack
0x0000000000455aac <+44>: movq $0x2,0x8(%rsp) # push the second parameter (2) of sum onto the stack
0x0000000000455ab5 <+53>: callq 0x455a20 <main.sum> # call sum
0x0000000000455aba <+58>: mov 0x18(%rsp),%rbp # restore rbp to the caller's rbp
0x0000000000455abf <+63>: add $0x20,%rsp # adjust rsp to point to the stack unit that stores the caller's return address
0x0000000000455ac3 <+67>: retq # return to the caller
0x0000000000455ac4 <+68>: callq 0x44ae90 <runtime.morestack_noctxt>
0x0000000000455ac9 <+73>: jmp 0x455a80 <main.main>
End of assembler dump.
(gdb) disass main.sum
Dump of assembler code for function main.sum:
0x0000000000455a20 <+0>: sub $0x20,%rsp # reserve 32 bytes of stack space for sum
0x0000000000455a24 <+4>: mov %rbp,0x18(%rsp) # save main's rbp
0x0000000000455a29 <+9>: lea 0x18(%rsp),%rbp # set sum's rbp
0x0000000000455a2e <+14>: movq $0x0,0x38(%rsp) # initialize return value to 0
0x0000000000455a37 <+23>: mov 0x28(%rsp),%rax # read the first parameter a (1) from memory into rax
0x0000000000455a3c <+28>: mov 0x28(%rsp),%rcx # read the first parameter a (1) from memory into rcx
0x0000000000455a41 <+33>: imul %rax,%rcx # calculate a * a and put the result in rcx
0x0000000000455a45 <+37>: mov %rcx,0x10(%rsp) # assign rcx (a * a) to variable a2
0x0000000000455a4a <+42>: mov 0x30(%rsp),%rax # read the second parameter a (2) from memory into rax
0x0000000000455a4f <+47>: mov 0x30(%rsp),%rcx # read the second parameter a (2) from memory into rcx
0x0000000000455a54 <+52>: imul %rax,%rcx # calculate b * b and put the result in rcx
0x0000000000455a58 <+56>: mov %rcx,0x8(%rsp) # assign rcx (b * b) to variable b2
0x0000000000455a5d <+61>: mov 0x10(%rsp),%rax # read a2 from memory into rax
0x0000000000455a62 <+66>: add %rcx,%rax # calculate a2 + b2 and store the result in rax
0x0000000000455a65 <+69>: mov %rax,(%rsp) # assign rax to variable c, c = a2 + b2
0x0000000000455a69 <+73>: mov %rax,0x38(%rsp) # copy rax (a2 + b2) to the return value
0x0000000000455a6e <+78>: mov 0x18(%rsp),%rbp # restore main's rbp
0x0000000000455a73 <+83>: add $0x20,%rsp # adjust rsp to point to the stack unit storing the return address
0x0000000000455a77 <+87>: retq # return to main
End of assembler dump.
6.4. Comparison
6.4.1. Parameter Passing
- C language
- x86-64 calling conventions normally pass arguments in registers first; the exact registers depend on the platform ABI, with additional arguments passed on the stack.
- In the Windows x64 example above, the first four integer or pointer arguments use
rcx,rdx,r8, andr9.
- Go language
- The historical example above uses a stack-based ABI, so its arguments are passed through the stack.
6.4.2. Return Values
- C language
- Common x86-64 ABIs use
raxfor integer or pointer return values.
- Common x86-64 ABIs use
- Go language
- The historical example above returns the function result through the stack.
6.4.3. Local Variables
- C language
- In the example above, which keeps a frame pointer, local and temporary variables are accessed through offsets from
rbp.
- In the example above, which keeps a frame pointer, local and temporary variables are accessed through offsets from
- Go language
- The historical example above accesses stack-resident local data through offsets from
rsp.
- The historical example above accesses stack-resident local data through offsets from
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub