NOTE

Assembly Language

Historical study notes on x86-64 AT&T assembly, registers, addressing, function calls, and C/Go disassembly.

Computer Architecture & AssemblyCreated Updated 4 min readhistorical

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

  • %rsp is 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.
  • %rbp is 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.
  • %rax is 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.

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 goto and if in 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 rsp register.
  • Format
push  source_operand
pop   destination_operand

5.6. leave

  • leave has no operands. It is generally placed at the end of a function before ret and is used to adjust rsp and rbp. 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 rbp register 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 rip is 0x1004010a2, pointing to the next instruction to execute.
  • The value of rsp is 0xffffcc18, pointing to the top of the stack.
  • The value of rbp is 0xffffcce0, 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, and r9.
  • 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 rax for integer or pointer return values.
  • 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.
  • Go language
    • The historical example above accesses stack-resident local data through offsets from rsp.

7. References

Discussion

Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub