0. Preface
As a serious Gopher, understanding assembly is a must. This assembly series of articles will introduce the basics of assembly around basic Go programs.
1. Go program to assembly
First look at a ridiculously simple example:
package main
func main() {
a := 1
print(a)
}
Run the program and output:
# go run
1
When using thego run
When you run a program, the code is compiled, linked, and executed to get the output, which is executed automatically, and there is no way to view the intermediate processes. We can use thedlv
See what this code does when executed.dlv
Load the code into memory and give it to the CPU for execution without losing control of the CPU. In other words, we are passing the code to the CPU at the bottom through thedlv
Debugging the CPU to see how the code is executed is very helpful to understand the execution of the program.
utilizationdlv debug
Debugging program:
# go mod init ex0
go: creating new : module ex0
go: to add module requirements and sums:
go mod tidy
# dlv debug
Type 'help' for list of commands.
(dlv)
utilizationdisass
You can view the application's assembly code, which in this case is the assembly code executed by the real machine. Assembly is the closest "language" to the machine, and translating to assembly helps us know what the machine is doing with our code.
(dlv) disass
TEXT _rt0_amd64_linux(SB) /usr/local/go/src/runtime/rt0_linux_amd64.s
=> rt0_linux_amd64.s:8 0x466d00 e95bc9ffff jmp $_rt0_amd64
As you can see from this assembly code, entering themain
Before the function, the machine executes thert0_linux_amd64.s
Assembly instruction at line 8. Viewrt0_linux_amd64.s
:
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include ""
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
JMP _rt0_amd64_lib(SB)
Line 8 is executed asJMP _rt0_amd64(SB)
Jump command.
utilizationsi
command for single-step debugging.si
is instruction-level debugging. The execution of thesi
The view is of the next instruction executed by the CPU:
(dlv) si
> _rt0_amd64() /usr/local/go/src/runtime/asm_amd64.s:16 (PC: 0x463660)
Warning: debugging optimized function
TEXT _rt0_amd64(SB) /usr/local/go/src/runtime/asm_amd64.s
=> asm_amd64.s:16 0x463660 488b3c24 mov rdi, qword ptr [rsp]
asm_amd64.s:17 0x463664 488d742408 lea rsi, ptr [rsp+0x8]
asm_amd64.s:18 0x463669 e912000000 jmp $runtime.rt0_go
The CPU executes theruntime/asm_amd64.s
assembly instructions in the Viewruntime/asm_amd64.s
:
// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking. This is the entry point for the program from the
// kernel for an ordinary -buildmode=exe program. The stack holds the
// number of arguments and the C-style argv.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
As you can see, there is a difference between the Go runtime's assembly and the actual assembly instructions executed by the machine. Here Go's assembly can be interpreted as a customized layer of assembly on top of the assembly, and it is important to note that the actual assembly executed by the machine is the translated Go assembly.
1.1 The main function stack
The focus of this article is not on single-step debugging of runtime's assembly instructions; we are using theb
To add a breakpoint to the main function, usec
The execution reaches a breakpoint and focuses on the execution in the main function:
(dlv) b
Breakpoint 1 set at 0x45feca for () ./:3
(dlv) c
> () ./:3 (hits goroutine(1):1 total:1) (PC: 0x45feca)
1: package main
2:
=> 3: func main() {
4: a := 1
5: print(a)
6: }
The program executes to the third line of the program.disass
View assembly instructions:
(dlv) disass
TEXT (SB) /root/go/src/foundation/ex0/
:3 0x45fec0 493b6610 cmp rsp, qword ptr [r14+0x10]
:3 0x45fec4 762b jbe 0x45fef1
:3 0x45fec6 55 push rbp
:3 0x45fec7 4889e5 mov rbp, rsp
=> :3 0x45feca* 4883ec10 sub rsp, 0x10
Assembly code shows execution to memory address0x45feca
At this point, the memory address stores the assembly instructionsub rsp, 0x10
The hexadecimal equivalent is4883ec10
The conversion to binary machine instructions is1001000100000111110110000010000
。
We need to introduce the implementation in segmentssub rsp, 0x10
instructions executed by the previous CPU for easier understanding.
First.cmp rsp, qword ptr [r14+0x10]
The instruction compares the value in the rsp register with the value in the [r14+0x10] register and stores the result of the comparison in the flag register.
Next, the commandjbe 0x45fef1
The result of the flag register will be read, if the comparison result rsp is less than or equal to [r14+0x10] then jump to memory0x45fef1
. View0x45fef1
The instructions stored in the
:3 0x45fef1 e8eacdffff call $runtime.morestack_noctxt
0x45fef1
Storedruntime.morestack_noctxt
function is called.
The semantics of the machine instructions make it harder to understand what these instructions are doing, translated into semantic information it means that if the stack space on the current main function stack is insufficient, callruntime.morestack_noctxt
Request more stack space.
Next, continue with the instructionpush rbp
. Before describing this instruction, it is necessary to describe the machine's registers, using theregs
command to view the machine's registers:
(dlv) regs
Rip = 0x000000000045feca
Rsp = 0x000000c00003e758
Rax = 0x000000000045fec0
Rbx = 0x0000000000000000
Rcx = 0x0000000000000000
Rdx = 0x00000000004751a0
Rsi = 0x00000000004c3160
Rdi = 0x0000000000000000
Rbp = 0x000000c00003e758
...
The machine has many kinds of registers, and we focus on theRip
,Rsp
cap (a poem)Rbp
Registers.
The Rip register stores the memory address at which the CPU is currently executing the instruction. Note here that the memory address in the program is a virtual address; there are no segment or offset addresses. The currentRip
Stored in the0x000000000045feca
The corresponding executed machine instructions are=> :3 0x45feca* 4883ec10 sub rsp, 0x10
。
Rsp
Registers are generally used as the top of the function stack to store the top address of the function stack.Rbp
Generally used to store the next instruction executed by the program, the function stack needs to know where the next instruction to be executed is when jumping (it's okay if it's not clear here, it will be covered in a subsequent article)
return topush rbp
command, which sets therbp
The register values are stacked. The stack is from the high address to the low address.Rsp
register will be reduced by 8 bytes. Thenmov rbp, rsp
command to set the currentrsp
The value of the register is assigned to therbp
, rbp
will exist as the bottom of the function stack.
Based on the above analysis, the current memory space of the stack can be drawn as follows:
Continue single-step executionsub rsp, 0x10
Command.rsp
downward0x10
This is formain
The function stack opens up stack space. rsp value is:
(dlv) regs
Rsp = 0x000000c00003e748
disass
View subsequently executed assembly instructions:
(dlv) disass
Sending output to pager...
TEXT (SB) /root/go/src/foundation/ex0/
...
=> :4 0x45fece 48c744240801000000 mov qword ptr [rsp+0x8], 0x1
:5 0x45fed7 e8e449fdff call $
:5 0x45fedc 488b442408 mov rax, qword ptr [rsp+0x8]
:5 0x45fee1 e87a50fdff call $
:5 0x45fee6 e8354afdff call $
:6 0x45feeb 4883c410 add rsp, 0x10
:6 0x45feef 5d pop rbp
mov qword ptr [rsp+0x8], 0x1
commander-in-chief (military)0x1
Put it in the [rsp+0x8] memory address. Use thex
command to view the value in a memory address:
x 0x000000c00003e750
0xc00003e750: 0x01
Next.mov rax, qword ptr [rsp+0x8]
Set the memory address[rsp+0x8]:0x000000c00003e750
is copied to the value of registerrax
In thecall $
Prints the value in the register (ignored here)call $
cap (a poem)call $
(Directive).
Before we execute the next commandadd rsp, 0x10
Before doing so, look at the current memory space usage.
main
function stackrbp
points to the bottom of the function stack.rsp
points to the top of the function stack in the[rsp+0x8]
address holds local variable 1.
Next, execute theadd rsp, 0x10
Reclaim stack space:
(dlv) si
> () ./:6 (PC: 0x45feef)
:6 0x45feeb* 4883c410 add rsp, 0x10
=> :6 0x45feef 5d pop rbp
(dlv) regs
Rsp = 0x000000c00003e758
Be aware that recycling only changesRsp
register value, the data in memory still exists, it's a stack segment, and the data isn't reclaimed by the garbage collector:
x 0x000000c00003e750
0xc00003e750: 0x01
Go ahead. Execute.pop rbp
Places the value originally stored at the bottom of the stack into therbp
in the register:
(dlv) regs
Rip = 0x000000000045feef
Rsp = 0x000000c00003e758
Rbp = 0x000000c00003e758
(dlv) si
> () ./:6 (PC: 0x45fef0)
:6 0x45feef 5d pop rbp
=> :6 0x45fef0 c3 ret
(dlv) regs
Rip = 0x000000000045fef0
Rsp = 0x000000c00003e760
Rbp = 0x000000c00003e7d0
final executionret
command exitmain
function.
At this point, we are done analyzing a simple program that prints local variables. In the next article, we will continue to look at how to handwrite a plan9 assembly.