1
0
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:
chai2010 2018-07-21 13:19:00 +08:00
parent 116a134d3b
commit 3151d1fe2f
2 changed files with 41 additions and 31 deletions

View File

@ -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架构图
![](../images/ch3-arch-amd64-01.ditaa.png)
寄存器是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的内存和寄存器的相互关系如下图
![](../images/ch3-arch-amd64-02.ditaa.png)
在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的内存和寄存器的相互关系如下图
![](../images/ch3-arch-amd64-02.ditaa.png)
在AMD64环境伪PC寄存器其实是IP指令计数器寄存器的别名。伪FP寄存器对应的是函数的帧指针一般用来访问函数的参数和返回值。伪SP栈指针对应的是当前函数栈帧的底部不包括参数和返回值部分一般用于定位局部变量。伪SP是一个比较特殊的寄存器因为还存在一个同名的SP真寄存器。真SP寄存器对应的是栈的顶部一般用于定位调用其它函数的参数和返回值。
当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如`(SP)``+8(SP)`没有标识符前缀为真SP寄存器`a(SP)``b+8(SP)`有标识符为前缀表示伪寄存器。
## Go函数调用规范
和C语言函数不同Go语言函数的参数和返回值完全通过栈传递。下面是Go函数调用时栈的布局图
![](../images/ch3-func-stack-frame-layout-01.ditaa.png)
在汇编定义函数时我们需要关注framesize和argsize分别对应函数帧大小和函数参数和返回值的大小。
因为函数的参数和返回值大小可以通过Go函数的签名解析得到因此argsize一般是可以省略的。需要注意的是输入参数和返回值依此从低地址向高地址顺序排列。同时每个参数的类型需要满足地址对齐要求。
帧大小相对复杂一点:其中包含函数的局部变量和调用其它函数时的参数和返回值空间。局部变量也是从低地址向高地址顺序排列的,因此它们和栈增长方向是相反的。
在最下面灰色的部分是调用函数后的返回地址。当执行CALL指令时会自动将SP向下移动并将返回地址和SP寄存器存入栈中。然后被调用的函数执行RET返回指令时先从栈恢复BP和SP寄存器接着取出的返回地址跳转到对应的指令执行。
## MOV指令
MOV指令是最重要的机器指令它不仅仅用于在寄存器和内存之间传输数据而且还可以用于处理数据的扩展和截断操作。

View File

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