写点什么

指令集架构、机器码与 Go 语言

  • 2019-12-04
  • 本文字数:4562 字

    阅读完需:约 15 分钟

指令集架构、机器码与 Go 语言

1.5 机器码生成

Go 语言编译的最后一个阶段就是根据 SSA 中间代码生成机器码了,这里谈的机器码生成就是在目标 CPU 架构上能够运行的代码,中间代码生成 一节简单介绍的从抽象语法树到 SSA 中间代码的处理过程,处理 SSA 的将近 50 个步骤中有一些过程严格上来说其实是属于机器码生成阶段的。


在将 SSA 中间代码降级(lower)的过程中,编译器将一些值重写成了目标 CPU 架构的特定值,降级的过程处理了所有机器特定的重写规则并且对代码进行了一定程度的优化;在 SSA 中间代码生成阶段的最后,Go 函数体的代码会被转换成一系列的 obj.Prog 结构体。

__1. 指令集架构

首先需要介绍的就是指令集架构了,虽然我们在第一节 编译过程概述 中曾经讲解过指令集架构的相关知识,但是在这里还是需要引入更多的指令集构知识。



指令集架构 是计算机的抽象模型,在很多时候也被称作架构或者计算机架构,它其实是计算机软件和硬件之间的接口和桥梁;一个为特定指令集架构编写的应用程序能够运行在所有支持这种指令集架构的机器上,也就说如果当前应用程序支持 x86_64 的指令集,那么就可以运行在所有使用 x86_64 指令集的机器上,这其实就是分层的作用,每一个指令集架构都定义了支持的数据结构、主内存和寄存器、类似内存一致和地址模型的语义、支持的指令集和 IO 模型,它的引入其实就在软件和硬件之间引入了一个抽象层,让同一个二进制文件能够在不同版本的硬件上运行。


如果一个编程语言想要在所有的机器上运行,它就可以将中间代码转换成使用不同指令集架构的机器码,这可比为不同硬件单独移植要简单的太多了。

__1.1. 分类

最常见的指令集架构分类方法就是根据指令的复杂度将其分为复杂指令集(CISC)和精简指令集(RISC),复杂指令集架构包含了很多特定的指令,但是其中的一些指令很少会被程序使用,而精简指令集只实现了经常被使用的指令,更不常用的操作都会通过子程序实现。


复杂指令集 的特点就是指令数目多并且复杂,每条指令的字节长度并不相等,x86 就是常见的复杂指令集处理器,它的指令长度大小范围非常广,从 1 到 15 字节不等,对于长度不固定的指令,计算机必须额外对指令进行判断,这需要付出额外的性能损失。


精简指令集 对指令的数目和寻址方式做了精简,大大减少指令数量的同时更容易实现,指令集中的每一个指令都使用标准的字节长度、执行时间相比复杂指令集会少很多,处理器在处理指令时也可以流水执行,提高了对并行的支持,作为一种常见的精简指令集处理器,amd 使用 4 个字节作为指令的固定长度,省略了判断指令的性能损失。


最开始的计算机使用复杂指令集是因为当时的计算机的性能和内存非常有限,业界需要尽可能地减少机器需要执行的指令,所以更倾向于高度编码、长度不等以及多操作数的指令,但是随着性能的飞速提升,就出现了精简指令集这种牺牲代码密度换取简单实现的设计,除此之外,硬件的飞速提升带来了更多的寄存器和更高的时钟频率,软件开发人员也不再直接接触汇编代码,而是通过编译器和汇编器生成指令,复杂的机器指定对于编译器来说很难利用,所以精简的指令更适合在这种场景下使用。

__1.2. 小结

复杂指令集和精简指令集的使用其实是一种权衡,经过这么多年的发展,两种指令集也相互借鉴和学习,与最开始刚被设计出来时已经有了较大的差别,对于软件工程师来讲,复杂的硬件设备对于我们来说已经是领域下两层的知识了,其实不太需要掌握太多,但是对指令集架构感兴趣的读者可以简单找一些资料开拓眼界。

__2. 机器码生成

机器码的生成在 Go 的编译器中主要由两部分协同工作,其中一部分是负责 SSA 中间代码降级和根据目标架构进行特定处理的 cmd/compile/internal/ssa 包,另一部分是负责生成机器码的 cmd/internal/obj,前者会将 SSA 中间代码转换成 obj.Prog 指令,后者作为一个汇编器会将这些指令最终转换成机器码完成这次的编译。

__2.1. SSA 降级

SSA 的降级过程是在中间代码生成的过程完成的,其中将近 50 轮处理过程中,lower 阶段就会将 SSA 转换成机器特定的操作,该阶段的入口方法就是 lower 函数:


func lower(f *Func) {    applyRewrite(f, f.Config.lowerBlock, f.Config.lowerValue)}
复制代码


applyRewrite 传入的两个函数 lowerBlocklowerValue 其实就是在 中间代码生成 阶段初始化 SSA 配置时确定的,这两个函数会分别转换一个函数中的代码块和代码块中的值。


假设目标机器使用 x86 的架构,最终会调用 rewriteBlock386rewriteValue386 两个函数,这两个函数是两个巨大的 switch/case,前者总共有 2000 多行,后者将近 700 行,相关的用于处理 x86 架构重写的函数总共有将近 30000 行代码,我们只节选其中的一段简单展示一下:


func rewriteValue386(v *Value) bool {    switch v.Op {    case Op386ADCL:        return rewriteValue386_Op386ADCL_0(v)    case Op386ADDL:        return rewriteValue386_Op386ADDL_0(v) || rewriteValue386_Op386ADDL_10(v) || rewriteValue386_Op386ADDL_20(v)    //...    }}
func rewriteValue386_Op386ADCL_0(v *Value) bool { // match: (ADCL x (MOVLconst [c]) f) // cond: // result: (ADCLconst [c] x f) for { _ = v.Args[2] x := v.Args[0] v_1 := v.Args[1] if v_1.Op != Op386MOVLconst { break } c := v_1.AuxInt f := v.Args[2] v.reset(Op386ADCLconst) v.AuxInt = c v.AddArg(x) v.AddArg(f) return true } // ...}
复制代码


重写的过程会将通用的 SSA 中间代码转换成目标架构特定的指令,上述代码就会使用 ADCLconst 替换 ADCLMOVLconst 两条指令。


buildssa 函数执行结束之后会继续执行 compileFunctions 中的 genssa 方法:


func compileSSA(fn *Node, worker int) {    f := buildssa(fn, worker)    pp := newProgs(fn, worker)    defer pp.Free()    genssa(f, pp)
pp.Flush()}
复制代码


该方法会创建一个新的 obj.Progs 结构并将生成的 SSA 中间代码都存入新建的结构体中,如果我们与在编译时加入了 GOSSAFUNC=hello 参数就会打印出最后生成的中间代码:


genssa hello# ./hello.go           00000 (3)    TEXT    "".hello(SB)           00001 (3)    FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)           00002 (3)    FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)           00003 (3)    FUNCDATA    $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) v8        00004 (4)    PCDATA    $2, $0 v8        00005 (4)    PCDATA    $0, $0 v8        00006 (4)    MOVQ    "".a(SP), AX v9        00007 (4)    ADDQ    $2, AX v11       00008 (5)    MOVQ    AX, "".~r1+8(SP) b1        00009 (5)    RET           00010 (?)    END
复制代码


上述输出结果跟最后生成的汇编代码其实已经非常相似了,随后调用的 Flush 函数就会使用 cmd/internal/obj 中的汇编器将 SSA 转换成汇编代码:


func (pp *Progs) Flush() {    plist := &obj.Plist{Firstpc: pp.Text, Curfn: pp.curfn}    obj.Flushplist(Ctxt, plist, pp.NewProg, myimportpath)}
复制代码


buildssa 中的 lower 阶段和随后的多个阶段会对 SSA 进行转换、检查和优化,接下来通过 genssa 将代码输出到 Progs 对象,这也是代码进入汇编器前的最后一个步骤。

__2.2. 汇编器

汇编器是将汇编语言翻译为机器语言的程序,Go 语言的汇编器是基于 Plan 9 汇编器 的输入类型,需要注意的是 Go 汇编器生成的代码并不是目标机器的直接表示,汇编器将一个半抽象的指令集转换成指令。我们将如下的代码编译成汇编指令,可以得到如下的内容:


$ cat hello.gopackage hello
func hello(a int) int { c := a + 2 return c}$ GOOS=linux GOARCH=amd64 go tool compile -S main.go"".hello STEXT nosplit size=15 args=0x10 locals=0x0 0x0000 00000 (main.go:3) TEXT "".hello(SB), NOSPLIT, $0-16 0x0000 00000 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.go:3) FUNCDATA $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (main.go:4) PCDATA $2, $0 0x0000 00000 (main.go:4) PCDATA $0, $0 0x0000 00000 (main.go:4) MOVQ "".a+8(SP), AX 0x0005 00005 (main.go:4) ADDQ $2, AX 0x0009 00009 (main.go:5) MOVQ AX, "".~r1+16(SP) 0x000e 00014 (main.go:5) RET 0x0000 48 8b 44 24 08 48 83 c0 02 48 89 44 24 10 c3 H.D$.H...H.D$..// ...
复制代码


这里的代码其实都是由 Flushplist 这个函数生成的,该函数会调用架构特定的 PreprocessAssemble 方法:


func Flushplist(ctxt *Link, plist *Plist, newprog ProgAlloc, myimportpath string) {    // ...
for _, s := range text { mkfwd(s) linkpatch(ctxt, s, newprog) ctxt.Arch.Preprocess(ctxt, s, newprog) ctxt.Arch.Assemble(ctxt, s, newprog) linkpcln(ctxt, s) ctxt.populateDWARF(plist.Curfn, s, myimportpath) }}
复制代码


这两个函数其实是在 Go 编译器最外层的主函数就确定了,它会从archInits 中选择当前结构的初始化方法并对当前架构使用的配置进行初始化。


如果目标的机器架构时 x86 的,那么这两个函数最终会使用 preprocessspan6,作者在这里就不展开介绍这两个特别复杂并且底层的函数了,有兴趣的读者可以通过上述链接找到目标函数的位置了解预处理和汇编的过程,最后的机器码生成过程也都是由这些函数组合完成的。

__3. 总结

机器码生成作为 Go 语言编译的最后一步,其实已经到了硬件和机器指令这一层,其中对于内存、寄存器的处理非常复杂并且难以阅读,想要真正掌握这里的处理的步骤和原理还是需要非常多的精力,但是作为软件工程师来说,如果不是 Go 语言编译器的开发者或者需要经常处理汇编语言和机器指令,掌握这些知识的投资回报率实在太低,没有太多的必要。


到这里,整个 Go 语言编译的过程也都介绍完了,从词法与语法分析类型检查中间代码生成到最后的机器码生成,包含的内容非常复杂,不过经过分析我们已经能够对 Go 语言编译器的原理有足够的了解,也对相关特性的实现更加清楚,后面的章节会介绍一些具体特性的原理,这些原理会依赖于编译期间的一些步骤,所以我们在深入理解 Go 语言的特性之前还是需要先了解一些编译期间完成的工作。

__4. Reference

__5. 其他

__5.1. 关于图片和转载

文章未经许可均禁止转载,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。


**本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/golang/compile/golang-machinecode.html


2019-12-04 08:00830

评论

发布
暂无评论
发现更多内容

Apache Doris (incubating) 1.0 Release 版本正式发布!

ApacheDoris

数据库 大数据 开源 OLAP apache doris

linux之rpm命令

入门小站

Linux

读《Software Engineering at Google》(08)

术子米德

架构师成长笔记

高效进行接口测试,简单易懂!

Liam

测试 Jmeter Postman swagger 测试工具

如何做好任务管理,手把手教你怎么做最高效的任务管理

阿里云云效

云计算 阿里云 云原生 研发团队 项目协作

Camtasia Studio2022汉化版

茶色酒

Camtasia2022

Web3.0 时代,我们的生活将产生什么变化?

CECBC

关于数字货币的几点问题及回应

CECBC

物联网低代码平台常用《组件介绍》

AIRIOT

开发 物联网 平台搭建、

过去一周热点回顾|Hoo虎符研究院 区块链简报 20220418期

区块链前沿News

虎符交易所

论利润中心内部核算和集团核算

秋去冬来春未远

阿米巴 利润中心 集团成本

博云 BeyondCMP 云管理平台 5.6 版本发布

BoCloud博云

云管理平台

利用 Dio 完成数据删除操作

岛上码农

ios 跨平台 移动端开发 flutter开发 安卓开发

安全之花如何盛开在华为云空间的每个角落?

脑极体

在线YAML转CSV工具

入门小站

工具

在线CSV转Plaintext(txt)工具

入门小站

工具

另一视角看元宇宙:元宇宙文化正悄然改变世界

CECBC

国产化云平台如何实现多云管控,黄河云来“打样儿”

BoCloud博云

国产化 云管理平台

深圳助力建设全国「数据交易」大市场,「隐私计算」技术赋能数据要素安全流通

洞见科技

【ELT.ZIP】OpenHarmony啃论文俱乐部——这些小风景你不应该错过

ELT.ZIP

神经网络 OpenHarmony ELT.ZIP

区块链如何助推著原创保护

CECBC

移动端日历组件设计与实现

CRMEB

企业管理理念之人本善还是本恶

秋去冬来春未远

企业管理 人性本善 人性本恶 一念之差

[Day19]-[动态规划]分割等和子集

方勇(gopher)

LeetCode 动态规划 数据结构和算法

OceanBase 杨传辉参与数据库技术与应用发展研讨会

OceanBase 数据库

oceanbase

【ELT.ZIP】OpenHarmony啃论文俱乐部——浅析稀疏表示医学图像

ELT.ZIP

OpenHarmony 医学影像 稀疏矩阵 ELT.ZIP

Java 操作 Office:POI word 之文档信息提取

程序员架构进阶

内容审核 4月日更 文档识别 4月月更

一文论述元宇宙、NFT及不可回避的Web3 时代

CECBC

以OceanBase为例,分析事务型评测基准对分布式数据库的适用性

OceanBase 数据库

分布式数据库 oceanbase

优秀程序员的30种思维(29/100)

hackstoic

技术思维

易周金融观点:遏制NFT金融化等打下监管良基

易观分析

NFT

指令集架构、机器码与 Go 语言_文化 & 方法_Draveness_InfoQ精选文章