mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 12:32:21 +00:00
ch3: 重构部分内容
This commit is contained in:
parent
116a134d3b
commit
3151d1fe2f
@ -11,7 +11,7 @@
|
||||
|
||||
图灵机虽然不容易编程,但是非常容易理解。有一种极小化的BrainFuck计算机语言,它的工作模式和图灵机非常相似。BrainFuck由Urban Müller在1993年创建的,简称为BF语言。Müller最初的设计目标是建立一种简单的、可以用最小的编译器来实现的、符合图灵完全思想的编程语言。这种语言由八种状态构成,早期为Amiga机器编写的编译器(第二版)只有240个字节大小!
|
||||
|
||||
就象它的名字所暗示的,brainfuck程序很难读懂。尽管如此,brainfuck图灵机一样可以完成任何计算任务。虽然brainfuck的计算方式如此与众不同,但它确实能够正确运行。这种语言基于一个简单的机器模型,除了指令,这个机器还包括:一个以字节为单位、被初始化为零的数组、一个指向该数组的指针(初始时指向数组的第一个字节)、以及用于输入输出的两个字节流。这种 语言,是一种按照“Turing complete(完整图灵机)”思想设计的语言,它的主要设计思路是:用最小的概念实现一种“简单”的语言,BrainF**k 语言只有八种符号,所有的操作都由这八种符号的组合来完成。
|
||||
就象它的名字所暗示的,brainfuck程序很难读懂。尽管如此,brainfuck图灵机一样可以完成任何计算任务。虽然brainfuck的计算方式如此与众不同,但它确实能够正确运行。这种语言基于一个简单的机器模型,除了指令,这个机器还包括:一个以字节为单位、被初始化为零的数组、一个指向该数组的指针(初始时指向数组的第一个字节)、以及用于输入输出的两个字节流。这是一种按照图灵完备的语言,它的主要设计思路是:用最小的概念实现一种“简单”的语言。BrainFuck 语言只有八种符号,所有的操作都由这八种符号的组合来完成。
|
||||
|
||||
下面是这八种状态的描述,其中每个状态由一个字符标识:
|
||||
|
||||
@ -80,17 +80,30 @@ LOOP:
|
||||
首先通过INBOX指令读取一个数据包;然后判断包裹的数据是否为0,如果是0的话就跳转到开头继续读取下一个数据包;否则将输出数据包,然后再跳转到开头。以此循环无休止地处理数据包裹,直到任务完成晋升到更高一级的岗位,然后处理类似的但更复杂的任务。
|
||||
|
||||
|
||||
## 精简X86-64指令集
|
||||
## X86-64体系结构
|
||||
|
||||
X86其实是是80X86的简称(后面三个字母),包括Intel 8086、80286、80386以及80486等指令集合,因此其架构被称为x86架构。x86-64是AMD公司于1999年设计的x86架构的64位拓展,向后兼容于16位及32位的x86架构。X86-64目前正式名称为AMD64,也就是Go语言中GOARCH环境变量指定的AMD64。如果没有特殊说明的话,本章中的汇编程序都是针对64位的X86-64环境。
|
||||
|
||||
很多汇编语言的教程都会强调汇编语言是不可移植的。严格来说很多汇编语言在不同的CPU类型、或不同的操作系统环境、或不同的汇编工具链下是不可移植的。而这种不可移植性正是汇编语言普及的一个极大的障碍。虽然CPU指令集的差异是导致不好移植的较大因素,但是汇编语言的相关工具链对此也有不可推卸的责任。而源自Plan9的Go汇编语言对此做了一定的改进:首先Go汇编语言在相同CPU架构上是完全一致的,也就是屏蔽了操作系统的差异;同时Go汇编语言将一些基础并且类似的指令抽象为相同名字的伪指令,从而减少不同CPU架构下汇编代码的差异(当然,寄存器名字和数量的差异是一直存在的)。本节的目的也是找出一个较小的精简指令集,以简化Go汇编语言的学习。
|
||||
|
||||
下面是X86/AMD架构图:
|
||||
在使用汇编语言之前必须要了解对应的CPU体系结构。下面是X86/AMD架构图:
|
||||
|
||||

|
||||
|
||||
寄存器是CPU中最重要的资源,每个要处理的内存数据原则上需要先放到寄存器中才能由CPU处理,同时寄存器中处理完的结果需要再存入内存。X86中除了状态寄存器和指令寄存器两个特殊的寄存器外,还有AX、BX、CX、DX、SI、DI、BP、SP几个通用寄存器。在X86-64中又增加了八个以R8-R15方式命名的通用寄存器。因为历史的原因R0-R7并不是通用寄存器,它们只是X87开始引入的MMX指令专有的寄存器。在通用寄存器中BP和SP是两个比较特殊的寄存器:其中BP用于记录当前函数帧的开始位置,和函数调用相关的指令会隐式地影响SP的值;SP则对应当前栈指针的位置,和栈相关的指令会隐式地影响SP的值。
|
||||
CPU是由指令和寄存器组成,其中指令是CPU内置的算法,而寄存器是指令对应的算法要操作的数据结构。因此寄存器是CPU中最重要的资源,每个要处理的内存数据原则上需要先放到寄存器中才能由CPU处理,同时寄存器中处理完的结果需要再存入内存。X86中除了状态寄存器FLAGS和指令寄存器IP两个特殊的寄存器外,还有AX、BX、CX、DX、SI、DI、BP、SP几个通用寄存器。在X86-64中又增加了八个以R8-R15方式命名的通用寄存器。因为历史的原因R0-R7并不是通用寄存器,它们只是X87开始引入的MMX指令专有的寄存器。在通用寄存器中BP和SP是两个比较特殊的寄存器:其中BP用于记录当前函数帧的开始位置,和函数调用相关的指令会隐式地影响SP的值;SP则对应当前栈指针的位置,和栈相关的指令会隐式地影响SP的值;而某些调试工具需要BP寄存器才能正常工作。
|
||||
|
||||
|
||||
## Go汇编中的伪寄存器
|
||||
|
||||
Go汇编为了简化汇编代码的编写,引入了PC、FP、SP、SB四个伪寄存器。四个伪寄存器和X86/AMD64的内存和寄存器的相互关系如下图:
|
||||
|
||||

|
||||
|
||||
在AMD64环境,伪PC寄存器其实是IP指令计数器寄存器的别名。伪FP寄存器对应的是函数的帧指针,一般用来访问函数的参数和返回值。伪SP栈指针对应的是当前函数栈帧的底部(不包括参数和返回值部分),一般用于定位局部变量。伪SP是一个比较特殊的寄存器,因为还存在一个同名的SP真寄存器。真SP寄存器对应的是栈的顶部,一般用于定位调用其它函数的参数和返回值。
|
||||
|
||||
当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如`(SP)`、`+8(SP)`没有标识符前缀为真SP寄存器,而`a(SP)`、`b+8(SP)`有标识符为前缀表示伪寄存器。
|
||||
|
||||
## X86-64指令集
|
||||
|
||||
很多汇编语言的教程都会强调汇编语言是不可移植的。严格来说汇编语言是在不同的CPU类型、或不同的操作系统环境、或不同的汇编工具链下是不可移植的,而在同一种CPU中运行的机器指令是完全一样的。汇编语言这种不可移植性正是其普及的一个极大的障碍。虽然CPU指令集的差异是导致不好移植的较大因素,但是汇编语言的相关工具链对此也有不可推卸的责任。而源自Plan9的Go汇编语言对此做了一定的改进:首先Go汇编语言在相同CPU架构上是完全一致的,也就是屏蔽了操作系统的差异;同时Go汇编语言将一些基础并且类似的指令抽象为相同名字的伪指令,从而减少不同CPU架构下汇编代码的差异(寄存器名字和数量的差异是一直存在的)。本节的目的也是找出一个较小的精简指令集,以简化Go汇编语言的学习。
|
||||
|
||||
X86是一个极其复杂的系统,有人统计x86-64中指令有将近一千个之多。不仅仅如此,X86中的很多单个指令的功能也非常强大,比如有论文证明了仅仅一个MOV指令就可以构成一个图灵完备的系统。以上这是两种极端情况,太多的指令和太少的指令都不利于汇编程序的编写。通用的基础机器指令大概可以分为数据传输指令、算术运算和逻辑运算指令、控制流指令等几类。因此我们将尝试精简出一个X86-64指令集,以便于Go汇编语言的学习。
|
||||
|
||||
@ -130,31 +143,6 @@ X86是一个极其复杂的系统,有人统计x86-64中指令有将近一千
|
||||
|
||||
为了简单我们省略了位运算指令,很多高级指令。完整的X86指令在 https://github.com/golang/arch/blob/master/x86/x86.csv 文件定义。同时Go汇编还正对一些指令定义了别名,具体可以参考这里 https://golang.org/src/cmd/internal/obj/x86/anames.go 。
|
||||
|
||||
## Go汇编中的伪寄存器
|
||||
|
||||
Go汇编为了简化汇编代码的编写,引入了PC、FP、SP、SB四个伪寄存器。四个伪寄存器和X86/AMD64的内存和寄存器的相互关系如下图:
|
||||
|
||||

|
||||
|
||||
在AMD64环境,伪PC寄存器其实是IP指令计数器寄存器的别名。伪FP寄存器对应的是函数的帧指针,一般用来访问函数的参数和返回值。伪SP栈指针对应的是当前函数栈帧的底部(不包括参数和返回值部分),一般用于定位局部变量。伪SP是一个比较特殊的寄存器,因为还存在一个同名的SP真寄存器。真SP寄存器对应的是栈的顶部,一般用于定位调用其它函数的参数和返回值。
|
||||
|
||||
当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如`(SP)`、`+8(SP)`没有标识符前缀为真SP寄存器,而`a(SP)`、`b+8(SP)`有标识符为前缀表示伪寄存器。
|
||||
|
||||
|
||||
## Go函数调用规范
|
||||
|
||||
和C语言函数不同,Go语言函数的参数和返回值完全通过栈传递。下面是Go函数调用时栈的布局图:
|
||||
|
||||

|
||||
|
||||
在汇编定义函数时,我们需要关注framesize和argsize,分别对应函数帧大小和函数参数和返回值的大小。
|
||||
|
||||
因为函数的参数和返回值大小可以通过Go函数的签名解析得到,因此argsize一般是可以省略的。需要注意的是,输入参数和返回值依此从低地址向高地址顺序排列。同时每个参数的类型需要满足地址对齐要求。
|
||||
|
||||
帧大小相对复杂一点:其中包含函数的局部变量和调用其它函数时的参数和返回值空间。局部变量也是从低地址向高地址顺序排列的,因此它们和栈增长方向是相反的。
|
||||
|
||||
在最下面灰色的部分是调用函数后的返回地址。当执行CALL指令时,会自动将SP向下移动,并将返回地址和SP寄存器存入栈中。然后被调用的函数执行RET返回指令时,先从栈恢复BP和SP寄存器,接着取出的返回地址跳转到对应的指令执行。
|
||||
|
||||
## MOV指令
|
||||
|
||||
MOV指令是最重要的机器指令,它不仅仅用于在寄存器和内存之间传输数据,而且还可以用于处理数据的扩展和截断操作。
|
||||
|
@ -2,6 +2,28 @@
|
||||
|
||||
在前面的章节中我们已经简单讨论过Go的汇编函数,但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数,也就是栈的大小是可以预期的,叶子函数也就是可以基本忽略爆栈的问题(如果已经爆了,那也是上级函数的问题)。如果没有爆栈问题,那么也就是不会有栈的分裂问题;如果没有栈的分裂也就不需要移动栈上的指针,也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的,永远不用担心爆栈的风险。那么这些近似黑科技的特性是如何通过低级的汇编语言实现的呢?这些都是本节尝试讨论的问题。
|
||||
|
||||
<!--
|
||||
TODO: CALL/RET 工作原理
|
||||
TODO: CALL/RET 前后插入的BP代码
|
||||
|
||||
汇编中 morestack 代码也可以自动生成
|
||||
-->
|
||||
|
||||
## Go函数调用规范
|
||||
|
||||
和C语言函数不同,Go语言函数的参数和返回值完全通过栈传递。下面是Go函数调用时栈的布局图:
|
||||
|
||||

|
||||
|
||||
在汇编定义函数时,我们需要关注framesize和argsize,分别对应函数帧大小和函数参数和返回值的大小。
|
||||
|
||||
因为函数的参数和返回值大小可以通过Go函数的签名解析得到,因此argsize一般是可以省略的。需要注意的是,输入参数和返回值依此从低地址向高地址顺序排列。同时每个参数的类型需要满足地址对齐要求。
|
||||
|
||||
帧大小相对复杂一点:其中包含函数的局部变量和调用其它函数时的参数和返回值空间。局部变量也是从低地址向高地址顺序排列的,因此它们和栈增长方向是相反的。
|
||||
|
||||
在最下面灰色的部分是调用函数后的返回地址。当执行CALL指令时,会自动将SP向下移动,并将返回地址和SP寄存器存入栈中。然后被调用的函数执行RET返回指令时,先从栈恢复BP和SP寄存器,接着取出的返回地址跳转到对应的指令执行。
|
||||
|
||||
|
||||
## 递归函数: 1到n求和
|
||||
|
||||
递归函数是比较特殊的函数,递归函数通过调用自身并且在栈上保存状态,这可以简化很多问题的处理。Go语言中递归函数的强大之处是不用担心爆栈问题,因为栈可以根据需要进行扩容和收缩。我们现在尝试通过汇编语言实现一个递归调用的函数,为了简化目前先不考虑栈的变化。
|
||||
|
Loading…
x
Reference in New Issue
Block a user