Go语言汇编

Go语言的汇编基于Plan 9的汇编,但是有一些不同。最主要的一个区别是,Go语言的汇编指令不一定直接对应机器表示。有一些直接对应,有一些则不是。



Go语言的汇编和Plan 9的另一个不同是操作符的优先级。比如3&1«2被解释成(3&1)«2。



介绍:
编译器产生的是一些力的中间码,具体的机器指令是在汇编生成之后才定下来的(Linker的工作)。



FUNCDATA和PCDATA是编译器产生的,用于保存一些给垃圾收集的信息。



符号:
Go语言有4个伪寄存器:



FP: 帧指针,保存参数和本地变量



PC:程序指针,负责跳转和流程控制



SB:静态基指针,全局变量



SP:栈指针,栈顶



所有的符号全部携程FP和SB的偏移的形式。



SB伪寄存器用来表示全局的变量或者函数,不如foo(SB)用来表示foo的地址。加<>表示符号本文件内可见。



FP是用来保存参数的。(0)FP是第一个参数(8)FP是第二个(如果是64位机器)。



SP指向本地栈顶,分别用x-8(SP), y-4(SP)表示变量。



直接的jmp或者call指令,只能指向text符号,不能是符号的偏移。



指令:
TEXT指令定义一个符号,后面紧跟函数体。



DATA指令定义一个section的内存,这段内存并不会被初始化。



DATA symbol+offset(SB)/width, value
GLOBAL指令定义一个符号是全局的



GLOBL divtab<>(SB), RODATA, $64



GLOBL runtime·tlsoffset(SB), NOPTR, $4
divtab是制度的64byte的表格,保存4个byte的整形。tlsoffset是,4byte的no pointers



指令修饰符:
DUPOK:允许一个二进制文件里有多个实例



NOSPLIT: FOR TEXT,routine或者routine的子函数,必须把栈的空间的头填满,用来保护栈分隔



RODATA:FOR DATA/GLOBL,把数据放在只读段



NOPTR: FOR DATA/GLOBL,数据没有指针,不需要被垃圾收集扫描



WRAPPER: FOR TEXT,wrapper function,不需要被以禁用recover计数



NEEDCTXT:FOR TEXT,闭包



Runtime协作:
NOPTR和RODATA的数据不需要被垃圾收集。比指针还要小的数据也被当做NOPTR。不要在go汇编里写非只读数据。

为什么要使用堆栈?
一个过程调用可以象跳转(jump)命令那样改变程序的控制流程, 但是与跳转不同的是, 当工作完成时,函数把控制权返回给调用之后的语句或指令。 这种高级抽象实现起来要靠堆栈的帮助。
堆栈也用于给函数中使用的局部变量动态分配空间, 同样给函数传递参数和函数返回值也要用到堆栈。



堆栈区详解
堆栈是一块保存数据的连续内存。 一个名为堆栈指针(SP)的寄存器指向堆栈的顶部。堆栈的底部在一个固定的地址。 堆栈的大小在运行时由内核动态地调整。



堆栈由逻辑堆栈帧组成。当调用函数时逻辑堆栈帧被压入栈中, 当函数返回时逻辑堆栈帧被从栈中弹出。 堆栈帧包括函数的参数, 函数地局部变量, 以及恢复前一个堆栈帧所需要的数据, 其中包括在函数调用时指令指针(IP)的值。 

堆栈既可以向下增长(向内存低地址)也可以向上增长, 这依赖于具体的实现。在我们的例子中, 堆栈是向下增长的。堆栈指针(SP)也是依赖于具体实现的。它可以指向堆栈的最后地址,或者指向堆栈之后的下一个空闲可用地址。 在我们的讨论当中, SP指向堆栈的最后地址。

除了堆栈指针(SP指向堆栈顶部的的低地址)之外, 为了使用方便还有指向帧内固定地址的指针叫做帧指针(FP)。有些文章把它叫做局部基指针(LB-local base pointer)。从理论上来说, 局部变量可以用SP加偏移量来引用。 然而, 当有字被压栈和出栈后, 这些偏移量就变了。 尽管在某些情况下编译器能够跟踪栈中的字操作, 由此可以修正偏移量, 但是在某些情况下不能。而且在所有情况下, 要引入可观的管理开销。 而且在有些机器上, 比如Intel处理器, 由SP加偏移量访问一个变量需要多条指令才能实现。

因此, 许多编译器使用第二个寄存器, FP, 对于局部变量和函数参数都可以引用, 因为它们到FP的距离不会受到PUSH和POP操作的影响。 在Intel CPU中, BP(EBP)用于这个目的。 在Motorola CPU中, 除了A7(堆栈指针SP)之外的任何地址寄存器都可以做FP。考虑到我们堆栈的增长方向, 从FP的位置开始计算, 函数参数的偏移量是正值, 而局部变量的偏移量是负值。

当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以恢复)。 然后它把SP复制到FP, 创建新的FP, 把SP向前移动为局部变量保留空间。 这称为例程的序幕(prolog)工作。当例程退出时, 堆栈必须被清除干净, 这称为例程的收尾(epilog)工作。 Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于有效地序幕和收尾工作。

Category golang