.... to be continued
Golang中很多代码实现,例如strings.Index(),调度器以及初始化等等都是用汇编实现的,因此需要对汇编有基本的了解。同时本文只专注于 AMD64 Linux 平台下 AT&T 格式的汇编指令。另外本文主要是作为一个学习笔记和总结,很多地方会引用参考文献中的内容。
基础知识
程序的存储空间布局
Program
是为了完成指定的任务而准备好的一个指令序列。
C编译器将每个源文件(source file
)翻译成object file
,然后编译器将单独地object files与必需的库链接起来形成一个可执行模块(executable module
)。当程序运行或执行时,操作系统将可执行模块拷贝到主内存中的程序镜像(program image
)里。
进程(Process)是一个正在执行的程序实例(instance)。每个实例有自己的地址空间和执行状态。当操作系统向内核数据结构中添加了适当的信息,并为运行程序代码分配必要的资源之后,程序就变成了进程。
线程是代表了进程内执行线程(a thread of execution within a process
)的一种抽象数据类型。线程有自己的执行栈,程序计数器值,寄存器集合和状态。
一个程序本质上都是由 BSS 段
、data段
、text段
三个组成的。可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。
BSS段(未初始化数据区)
:在采用段式内存管理的架构中,BSS段(bss segment
)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol
的简称。BSS段属于静态内存分配。
数据段
:在采用段式内存管理的架构中,数据段(data segment
)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
代码段
:在采用段式内存管理的架构中,代码段(text segment
)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
可以通过size
命令查看可执行二进制程序的section size
以及total size
,通过指定-A
参数使其按照System V
size输出。默认是按照Berkeley
size输出。
(ENV) [root@ceph-2 ~]# size bazil
text data bss dec hex filename
8373080 315568 144968 8833616 86ca50 bazil
可执行程序在运行时又多出两个区域:栈区
和堆区
。
- 栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
- 堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的 malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
指令集
可以通过如下的命令查询CPU支持的指令集。
(ENV) [root@ceph-2 ~]# cat /proc/cpuinfo | grep flags
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts nopl xtopology tsc_reliable nonstop_tsc aperfmperf pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx hypervisor lahf_lm tpr_shadow vnmi ept vpid tsc_adjust dtherm arat pln pts
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts nopl xtopology tsc_reliable nonstop_tsc aperfmperf pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx hypervisor lahf_lm tpr_shadow vnmi ept vpid tsc_adjust dtherm arat pln pts
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts nopl xtopology tsc_reliable nonstop_tsc aperfmperf pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx hypervisor lahf_lm tpr_shadow vnmi ept vpid tsc_adjust dtherm arat pln pts
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts nopl xtopology tsc_reliable nonstop_tsc aperfmperf pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx hypervisor lahf_lm tpr_shadow vnmi ept vpid tsc_adjust dtherm arat pln pts
汇编知识介绍
AT&T 汇编指令的基本格式为:
操作码 [操作数]
如果操作数有两个,则第一个为源操作数,第二个为目的操作数,目的操作数表示这条指令执行完后结果应该保存的地方。
对于 AT&T 格式的汇编指令,一些说明如下:
- 寄存器名需要加
%
作为前缀,立即数前加$
。 - 寄存器间接寻址的格式为
offset(%register)
,如果 offset 为 0,则可以略去偏移不写直接写成(%register)
。 - 与内存相关的一些指令的操作码会加上
b
,w
,l
和q
字母分别表示操作的内存是1
,2
,4
还是8
个字节,比如指令 movl $0x0,-0x8(%rbp) ,操作码 movl 的后缀字母 l 说明我们要把从 -0x8(%rbp) 这个地址开始的 4 个内存单元赋值为 0。
寄存器
应用层代码一般会用到三类 19 个寄存器:
-
通用寄存器
(64 位):rax, rbx, rcx, rdx, rsi, rdi,rbp
,rsp
, r8, r9, r10, r11, r12, r13, r14, r15 寄存器。CPU 对这 16 个通用寄存器的用途没有做特殊规定,程序员和编译器可以自定义其用途(下面会介绍,rsp/rbp 寄存器其实是有特殊用途的); -
程序计数寄存器
(64 位,PC寄存器,有时也叫 IP 寄存器):rip
寄存器。它用来存放下一条即将执行的指令的地址,这个寄存器决定了程序的执行流程; -
段寄存器
:fs
和gs
寄存器(两个都是 16 位)。一般用它来实现线程本地存储(TLS)
,比如 AMD64 linux 平台下 go 语言和 pthread 都使用 fs 寄存器来实现系统线程的 TLS。
有4个核心的伪寄存器
,这4个寄存器是编译器用来维护上下文、特殊标识等作用的:
-
FP(Frame pointer)
: arguments and locals -
PC(Program counter)
: jumps and branches -
SB(Static base pointer)
: global symbols -
SP(Stack pointer)
: top of stack
Plan 9汇编
LEA
和 MOV
,其中LEA用于操作地址;而MOV用于操作数据。例如:
LEAQ 8(SP), SI // argv 把 8(SP)地址放入 SI 寄存器中
MOVQ 0(SP), DI // argc 把0(SP)内容放入 DI 寄存器中
References
- Linux下C程序的存储空间布局
- 汇编 is so easy
- golang 汇编
- Dropping down: Go functions in assembly language
- golang内核系列--深入理解plan9汇编&实践
- Go语言goroutine调度器初始化
- Go scheduler 源码阅读
- plan9
有疑问加站长微信联系(非本文作者)