返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

11.1 ASM

发布于 2024-10-12 19:15:50 字数 6543 浏览 0 评论 0 收藏 0

基于 Plan 9 汇编风格。
保存在 .s 文件中,编译器自动编译、链接。

从 1.17 开始,使用寄存器传参,详情参考《下卷:调用约定》。

本文依然采用 stack-based 传参模式,注意区分!

指令

Intel                 AT&T                   Go
--------------------------------------------------------------
mov eax, 1            movl $1, %eax           MOVQ $1, AX
mov rbx, 0ffh         movl $0xff, %rbx        MOVQ $(0xff), BX
mov ecx, [ebx+3]      movl 3(%ebx), %ecx      MOVQ 2(BX), CX

指令参数长度。

  • MOVB : 1-byte
  • MOVW : 2
  • MOVL : 4
  • MOVQ : 8

数据移动方向:从左往右。

ADD R1, R2        // R2 += R1
SUB R3, R4        // R4 -= R3
SUB R3, R4, R5    // R5 = R4 - R3
MUL $7, R6        // R6 *= 7

内存访问。

MOV (R1), R2            // R2 = *R1
MOV 8(R1), R2           // R2 = *(8 + R1)
MOV 16(R1)(R2*2), R3    // R3 = *(16 + R1 + R2*2)
MOV runtime·x(SB), R2   // R2 = *runtime·x

跳转指令。

JMP label      // 跳转到标签。
JMP 2(PC)      // 跳转到 PC + n 行。
JMP -2(PC)

数字常量以 $ 开头。十进制( $10 )和 十六进制( $0x10 )。
编译器引入 FUNCDATAPCDATA ,包含垃圾回收信息。

寄存器

伪寄存器(pseudo-register)由汇编语言定义并使用,最终会被编译器替换为硬件寄存器。

go build -gcfalgs -S 输出结果已没有伪寄存器。

go tool objdump 结果也没有伪寄存器,且更干净。

  • SB: Static Base Pointer(静态基指针)

    表示内存起始位置,通常用于全局函数或数据。

    例如, CALL add(SB) 表示函数 add 地址。


    添加尖括号( add<>(SB) ),表示仅当前文件內可见,私有成员。

    还可添加偏移量,表示基于符号的地址,例如 add+8(SB) 。

  • FP: Frame Pointer(参数地址)

    指向参数列表起始地址,以偏移量指向不同参数或返回值。

    偏移量前包含参数名: symbol+offset(FP) 。

    如果没有参数名,无法编译。

    例如, size+16(FP) 表示 size = (FP + 16)

  • SP: Stack Pointer(栈局部变量内存地址)

    当前栈帧內,指向本地局部变量起始地址。
    
鉴于栈从底开始,所有偏移量通常是负数( [-framesize, 0) )。
    注意添加变量名, symbol+offset(SP) 。例如, x-8(SP)

    提示:对应 BP 寄存器值,没有符号名时表示硬件 SP 寄存器。
  • PC: Program Counter(指令地址)

    按指令行数跳转。

    例如, JMP 2(PC) 以当前位置为 0 基准,往下跳 2 行。
// add.s

#include "textflag.h"

// add(x, y int) int
TEXT ·add(SB), NOSPLIT, $8-24
    MOVQ $0, z-0x8(SP)
    MOVQ x+0x0(FP), AX
    MOVQ y+0x8(FP), BX
    ADDQ AX, BX       
    MOVQ BX, z-0x8(SP)
    MOVQ BX, ret+0x10(FP)
    RET
package main

func add(x, y int) (z int)    // 声明汇编函数原型

func main() {
    z := add(0x100, 0x200)
    _ = z
}
$ go build -gcflags "-N -l" -o test

$ go tool objdump -s "main\.main" ./test
TEXT main.main(SB) main.go

  main.go:5     0x455200      CMPQ 0x10(R14), SP
  main.go:5     0x455204      JBE 0x45524b
  main.go:5     0x455206      SUBQ $0x28, SP
  main.go:5     0x45520a      MOVQ BP, 0x20(SP)
  main.go:5     0x45520f      LEAQ 0x20(SP), BP
  
  main.go:6     0x455214      MOVQ $0x100, 0(SP)      ; arg0
  main.go:6     0x45521c      MOVQ $0x200, 0x8(SP)    ; arg1
  main.go:6     0x455225      CALL main.add.abi0(SB)
  
  main.go:6     0x45522a      XORPS X15, X15
  main.go:6     0x45522e      MOVQ FS:0xfffffff8, R14
  main.go:6     0x455237      MOVQ 0x10(SP), AX       ; add_ret
  main.go:6     0x45523c      MOVQ AX, 0x18(SP)       ; z
  
  main.go:8     0x455241      MOVQ 0x20(SP), BP
  main.go:8     0x455246      ADDQ $0x28, SP
  main.go:8     0x45524a      RET
  main.go:5     0x45524b      CALL runtime.morestack_noctxt.abi0(SB)
  main.go:5     0x455250      JMP main.main(SB)


$ go tool objdump -s "main\.add" ./test
TEXT main.add.abi0(SB) add.s

  add.s:4       0x455260      SUBQ $0x10, SP
  add.s:4       0x455264      MOVQ BP, 0x8(SP)
  add.s:4       0x455269      LEAQ 0x8(SP), BP
  
  add.s:5       0x45526e      MOVQ $0x0, 0(SP)
  add.s:6       0x455276      MOVQ 0x18(SP), AX  ; arg0
  add.s:7       0x45527b      MOVQ 0x20(SP), BX  ; arg1
  add.s:8       0x455280      ADDQ AX, BX
  add.s:9       0x455283      MOVQ BX, 0(SP)     ; z
  add.s:10      0x455287      MOVQ BX, 0x28(SP)  ; ret
  
  add.s:11      0x45528c      MOVQ 0x8(SP), BP
  add.s:11      0x455291      ADDQ $0x10, SP
  add.s:11      0x455295      RET

对函数(callee)而言, FP 实际指向调用者(caller)的栈帧。

就是说,调用者负责参数和返回值内存。

编译器自动计算需要插入的位置偏移(现场保护, BP / PC ),无需手工处理。

hardware-register                          pseudo-register

               SP   0x0  +=========+
                         | z       |
          add  BP   0x8  +---------+        SP  add.z-0x8(SP)
                         | BP      |
                   0x10  +=========+
    -------------------  | IP/call |  --------------------------
               SP  0x18  +=========+  0x0   FP  add.x+0x0(FP)
                         | arg0    |
                   0x20  +---------+  0x8       add.y+0x8(FP)
                         | arg1    |
                   0x28  +---------+  0x10
         main            | ret     |
                   0x30  +---------+  0x18
                         |         |
               BP        +---------+  0x20  SP
                         | BP      |
                         +=========+  0x28
   

不同于 NASM 使用 section 定义,直接在符号前添加类别。

  • DATA : 初始化全局符号内存。(未初始化的为零)
  • GLOBL : 声明符号是全局的。
DATA  symbol+offset(SB)/width, value
GLOBL symbol(SB), [width]
DATA world<>+0(SB)/8, $"hello wo"
DATA world<>+8(SB)/4, $"rld "

GLOBL world<>+0(SB),  RODATA, $12

RODATA 外,还有 NOPTR 表示没有指针,无需 GC。

函数

函数定义,指定栈帧和参数大小。

                               参数及返回值大小
                               | 
TEXT main·add(SB), NOSPLIT, $8-24
      |    |                 |
     包名 函数名               栈帧大小(不包括参数、返回值,以及环境保存)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文