Some confusion about golang assembly
Asked Answered
T

2

5

My Golang source code is as follows.

package main

func add(x, y int) int {
    return x + y
}

func main() {
    _ = add(1, 2)
}

The assembly code I obtained using go tool compile -N -l -S main.go > file1.s is as follows(part of it).

;file1.s
"".main STEXT size=54 args=0x0 locals=0x18 funcid=0x0
    0x0000 00000 (main.go:7)    TEXT    "".main(SB), ABIInternal, $24-0
    0x0000 00000 (main.go:7)    CMPQ    SP, 16(R14)
    0x0004 00004 (main.go:7)    PCDATA  $0, $-2
    0x0004 00004 (main.go:7)    JLS 47
    ……
    0x002f 00047 (main.go:7)    CALL    runtime.morestack_noctxt(SB)
    0x0034 00052 (main.go:7)    PCDATA  $0, $-1
    0x0034 00052 (main.go:7)    JMP 0

And the assembly code I obtained using go tool compile -N -l main.go and go tool objdump -S -gnu main.o > file2.s is as follows(part of it).

;file2.s
TEXT "".main(SB) gofile..D:/code/Test/025_go/007_ass/main.go
func main() {
  0x5b6         493b6610        CMPQ 0x10(R14), SP      // cmp 0x10(%r14),%rsp  
  0x5ba         7629            JBE 0x5e5               // jbe 0x5e5
  ……
  func main() {
  0x5e5         e800000000      CALL 0x5ea              // callq 0x5ea  [1:5]R_CALL:runtime.morestack_noctxt    
  0x5ea         ebca            JMP "".main(SB)         // jmp 0x5b6    

My questions are:

  1. Why are the source and destination of the CMPQ instructions in file1.s and file2.s opposite, as in CMPQ SP, 16(R14) vs CMPQ 0x10(R14), SP?
  2. For the above two code, my understanding is: when SP <= R14 + 16, call runtime.morestack_noctxt to extend stack. But what I don't understand is: why is SP <= R14 + 16, what is the logic behind? R14 is link register?
  3. Is the code in file2.s a dead loop? Why is it so? Why is the code in file1.s not a dead loop?
  4. What is the meaning of [1:5] in [1:5]R_CALL:runtime.morestack_noctxt in file2.s?

I have a basic knowledge of c++/golang as well as assembly, and I understand the memory layout of programs, but I am really confused about the above questions. Can anyone help me, or what material should I read?

Thank you to everyone who helps me.

Tag answered 12/5, 2022 at 14:29 Comment(2)
Go's asm syntax is based on PDP-11, like AT&T syntax for x86. op src, dst. cmp can usefully be used with operands in either order, depending on how you want FLAGS to be set. cmp reg,mem / jbe jumps if mem <= reg (unsigned). The semantic meaning with be = <= is backwards because it uses an operand-order other than Intel's. Like other ALU instructions that data back to 8086, it has two opcodes for each operand-size, cmp r/m, r and cmp r, r/m. felixcloutier.com/x86/cmp. Is any of that what you're asking?Stearin
Oh, those are supposed to be the same instruction?? Yeah like Fuz said, that seems like a bug.Stearin
C
8

Why are the source and destination of the CMPQ instructions in file1.s and file2.s opposite, as in CMPQ SP, 16(R14) vs CMPQ 0x10(R14), SP?

This is likely a bug in the disassembler which I encourage you to file with the Go project. The Go assembler has AT&T operand order for almost all instructions. The CMP family of instructions is a major exception which for easier use has the Intel operand order (i.e. CMPQ foo, bar; JGT baz jumps to baz if foo > bar).

For the above two code, my understanding is: when SP <= R14 + 16, call runtime.morestack_noctxt to extend stack. But what I don't understand is: why is SP <= R14 + 16, what is the logic behind? R14 is link register?

R14 holds a pointer to the g structure corresponding to the currently active Go routine and 0x10(R14) holds the stack limit stackguard0. See user1856856's answer for details. This is a new development following the register ABI proposal. stackguard0 lowest stack address the thread can use before it has to ask the runtime for more stack.

Is the code in file2.s a dead loop? Why is it so? Why is the code in file1.s not a dead loop?

No. When runtime.morestack_noctxt returns, it has changed R14 to the new stack limit, hence the comparison will succeed. It is possible that it will not succeed in which case once again more stack is allocated until it does. This means it is normally not an endless loop.

What is the meaning of [1:5] in [1:5]R_CALL:runtime.morestack_noctxt in file2.s?

This comments hints on the presence of a relocation, indicating that the linker will have to patch in the address of runtime.morestack_noctxt at link time. You can see that the function address in the instruction e800000000 is all zeroes, so as is the call doesn't go anywhere useful. This only changes when the linker resolves the relocation.

Careworn answered 12/5, 2022 at 14:39 Comment(0)
B
1

for question 2,in amd64 system, R14 is the register that holds the current g,you can check the function

// Append code to p to check for stack split.
// Appends to (does not overwrite) p.
// Assumes g is in rg.
// Returns last new instruction and G register.
func stacksplit(ctxt *obj.Link, cursym *obj.LSym, p *obj.Prog, newprog obj.ProgAlloc, framesize int32, textarg int32) (*obj.Prog, int16) {
// emit...
// Load G register
    var rg int16
    p, rg = loadG(ctxt, cursym, p, newprog)

    var q1 *obj.Prog
    if framesize <= objabi.StackSmall {
        // small stack: SP <= stackguard
        //  CMPQ SP, stackguard
        p = obj.Appendp(p, newprog)

        p.As = cmp
        p.From.Type = obj.TYPE_REG
        p.From.Reg = REG_SP
        p.To.Type = obj.TYPE_MEM
        p.To.Reg = rg
        p.To.Offset = 2 * int64(ctxt.Arch.PtrSize) // G.stackguard0
        if cursym.CFunc() {
            p.To.Offset = 3 * int64(ctxt.Arch.PtrSize) // G.stackguard1
        }

and the loadG

func loadG(ctxt *obj.Link, cursym *obj.LSym, p *obj.Prog, newprog obj.ProgAlloc) (*obj.Prog, int16) {
    if ctxt.Arch.Family == sys.AMD64 && cursym.ABI() == obj.ABIInternal {
        // Use the G register directly in ABIInternal
        return p, REGG
    }

    var regg int16 = REG_CX
    if ctxt.Arch.Family == sys.AMD64 {
        regg = REGG // == REG_R14
    }

and the file in src/cmd/internal/obj/x86/a.out.go

REGG         = REG_R14     // g register in ABIInternal

the g structure is

type g struct {
    // Stack parameters.
    // stack describes the actual stack memory: [stack.lo, stack.hi).
    // stackguard0 is the stack pointer compared in the Go stack growth prologue.
    // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
    // stackguard1 is the stack pointer compared in the C stack growth prologue.
    // It is stack.lo+StackGuard on g0 and gsignal stacks.
    // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
    stack       stack   // offset known to runtime/cgo
    stackguard0 uintptr // offset known to liblink
    stackguard1 uintptr // offset known to liblink
}

and the stack structure is

// Stack describes a Go execution stack.
// The bounds of the stack are exactly [lo, hi),
// with no implicit data structures on either side.
type stack struct {
    lo uintptr
    hi uintptr
}

so i think an offset(16) to R14 is the value of current g's stackguard0

Boughten answered 9/6, 2022 at 2:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.