diff --git a/ch3-asm/ch3-02-arch.md b/ch3-asm/ch3-02-arch.md index 5dd941f..24072d1 100644 --- a/ch3-asm/ch3-02-arch.md +++ b/ch3-asm/ch3-02-arch.md @@ -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指令是最重要的机器指令,它不仅仅用于在寄存器和内存之间传输数据,而且还可以用于处理数据的扩展和截断操作。 diff --git a/ch3-asm/ch3-06-func-again.md b/ch3-asm/ch3-06-func-again.md index e62321b..da0b0e2 100644 --- a/ch3-asm/ch3-06-func-again.md +++ b/ch3-asm/ch3-06-func-again.md @@ -2,6 +2,28 @@ 在前面的章节中我们已经简单讨论过Go的汇编函数,但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数,也就是栈的大小是可以预期的,叶子函数也就是可以基本忽略爆栈的问题(如果已经爆了,那也是上级函数的问题)。如果没有爆栈问题,那么也就是不会有栈的分裂问题;如果没有栈的分裂也就不需要移动栈上的指针,也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的,永远不用担心爆栈的风险。那么这些近似黑科技的特性是如何通过低级的汇编语言实现的呢?这些都是本节尝试讨论的问题。 + + +## 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语言中递归函数的强大之处是不用担心爆栈问题,因为栈可以根据需要进行扩容和收缩。我们现在尝试通过汇编语言实现一个递归调用的函数,为了简化目前先不考虑栈的变化。