Location>code7788 >text

Go plan9 compilation: Getting through the application to the ground floor

Popularity:869 ℃/2024-08-31 17:51:09

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, 0x10The hexadecimal equivalent is4883ec10The 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 theRipRsp 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 the0x000000000045fecaThe 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:

image

Continue single-step executionsub rsp, 0x10 Command.rsp downward0x10This 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.

image

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.