程序的机器级表示
计算机执行机器代码。编译器基于编程语言、目标机器的指令集以及操作系统,生成机器代码。
汇编代码是机器代码的文本表示。
对于严谨的程序员来说,能阅读和理解汇编代码形式是一项重要的技能。
对于处理器的机器语言,比较常见的就是x86-64,IA32是他的32位前身。
摩尔定律
各类处理器中的晶体管数量与他们出现的年份呈现正比例增长的趋势,晶体管基本每26个月都会翻一番。
预测未来10年,芯片上的晶体管每年都会翻一番,这个预测就是摩尔定律。
程序编码
GCC C编译器,这是linux上的默认编译器,实际上GCC调用了一整套的程序,将源代码转化为可执行代码。
首先,C预处理器扩展源代码,插入所有#include命令指定的文件,并扩展所有用#include声明的宏。
其次,编译器会产生两个源文件的汇编代码,后缀为.s。
接下来,汇编器会将汇编代码转化为二进制目标代码文件,后缀为.o。(目标代码是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入全局值的地址。)
最后,链接器将两个目标代码文件与实现库函数(例如printf)的代码合并,并产生最终的可执行代码文件。
机器级代码
计算机系统使用了不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。
ISA指令集体系结构或者指令集架构来定义机器级程序的格式和行为,他定义处理器状态、指令的格式,以及每条指令对状态的影响。
机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去一个非常大的字节数组。
在整个编译过程中,编译器完成大部分的工作,汇编代码非常接近于机器代码,他的主要特点是他的可读性更好。
x86-64的机器代码与C代码差别很大,一些通常对C程序员隐藏的处理器状态都是可见。
PC(计数器,在%rip来表示)给出将要执行的下一条指令在内存中的地址。
整数寄存器,包含16个命名的位置,分别存储64位的值,这些寄存器可以存储地址或整数数据,有些寄存器被用来记录重要的程序状态,其他的寄存器用来保存临时数据。
条件码寄存器,保存最近执行的算术或逻辑指令的状态信息。
向量寄存器,存放一个或多个整数或浮点数值。
程序内存包括:用来管理过程调用和返回的运行时栈,以及用户分配的内存块。
一条机器指令只执行一个非常基本的操作。
在C语言程序经过预处理与编译之后,就能得到汇编文件,代码中已经出去了所有的局部变量名或数据类型的信息。
但是机器执行的程序只是一个简单的字节序列,它是对一系列指令的编码,机器对产生这些指令的源代码几乎一无所知。
要查看机器代码的内容,反汇编器的程序就有作用,他会根据机器代码产生一种类似汇编代码的格式。并且不会去访问汇编代码或者源代码。
关于格式的注解
.file "Node.c"
.text
.globl InitList
.def InitList; .scl 2; .type 32; .endef
.seh_proc InitList
InitList:
subq $40, %rsp
.seh_stackalloc 40
.seh_endprologue
movl $16, %ecx
call malloc
movq $0, 8(%rax)
addq $40, %rsp
ret
.seh_endproc
.section .rdata,"dr"
如上是一段C语言代码经过GCC编译后的内容,它包含我们不需要关心的信息,另一方面,他不提供任何程序的描述或者他如何工作的描述。
所有以“.”开头的行都是知道汇编器和链接器工作的伪指令,通常可以忽略掉他们。
一种汇编语言程序员的代码风格是,只会给出与讨论内容相关的代码行,每一行的左边有行号提供引用,右边是注释。
还有网络旁注,提供一些资料,一个网络旁注者描述的是IA32的机器代码,有了x86-64的背景,学习IA32会相当简单,除此之外,它还描述了在C语言中插入汇编语言的方法。
ATT与Intel汇编格式
一般的表示,包括如上的表示都是AT&T的,这也是很多工具的默认的编码格式,其他的一些编程工具,都是Intel的,这两种格式在许多的方面有所不同。
.file "Node.c"
.intel_syntax noprefix
.text
.globl InitList
.def InitList; .scl 2; .type 32; .endef
.seh_proc InitList
InitList:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
mov ecx, 16
call malloc
mov QWORD PTR 8[rax], 0
add rsp, 40
ret
.seh_endproc
.section .rdata,"dr"
使用如上的命令行,GCC可以产生Intel格式的代码。
Intel代码省略了指示大小的后缀。
Intel代码省略了寄存器名字前面的“%”符号。
Intel代码用不同的方式来描述在内存中的位置。
在带有多个操作数的指令情况下,列出操作数的顺序相反。
数据格式
32位的体系结构是由16位扩展而来的,在Intel中用术语“字(word)”来表述16位数据类型,因此,32位数据类型成为双字,64位数称为四字。
C语言数据类型在x86-64中的大小。
C声明 | Intel数据类型 | 汇编代码后缀 | 大小(字节) |
---|---|---|---|
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
住:指针在64位的系统中,长8个字节。
大多数GCC产生的汇编代码指令中都有一个字符的后缀,表明操作数的大小,例如数据传送指令的变种:movb(字节)、movw(字)、movl(双字)、movq(四字)。
访问信息
一个x86-64的CPU包含一组16个存储64位值的通用目的寄存器,这些寄存器用来存储整数数据和指针,他们都以%r开头,最初的8086中有8个16位的寄存器,扩展到IA32时,这些寄存器也扩展为32位寄存器,标号从%eax到%ebp。
64位 | 32位 | 16位 | 8位 | 描述 |
---|---|---|---|---|
%rax | %eax | %ax | %al | 返回值 |
%rbx | %ebx | %bx | %bl | 被调用者保存 |
%rcx | %ecx | %cx | %cl | 第4个参数 |
%rdx | %edx | %dx | %dl | 第3个参数 |
%rsi | %esi | %si | %sil | 第2个参数 |
%rdi | %edi | %di | %dil | 第1个参数 |
%rbp | %ebp | %bp | %bpl | 被调用者保存 |
%rsp | %esp | %sp | %spl | 栈指针 |
%r8 | %r8d | %r8w | %r8b | 第5个参数 |
%r9 | %r9d | %r9w | %r9b | 第6个参数 |
%r10 | %r10d | %r10w | %r10b | 调用者保存 |
%r11 | %r11d | %r11w | %r11b | 调用者保存 |
%r12 | %r12d | %r12w | %r12b | 被调用者保存 |
%r13 | %r13d | %r13w | %r13b | 被调用者保存 |
%r14 | %r14d | %r14w | %r14b | 被调用者保存 |
%r15 | %r15d | %r15w | %r15b | 被调用者保存 |
整数寄存器,所有16个寄存器的低位部分都可以作为字节、字、双字和四字来访问。
在如上的寄存器中,esp与ebp较为特殊,前者是栈指针,后者为帧指针。其余的都可以通用。
操作数指示符
大多数指令都有一个或多个操作数,指示出一个操作中要使用的源数据值,以及放置结果的目的位置。
操作数被分为三种类型:
立即数:表示常数值,在AT&T中,立即数的书写方式是“$”后面跟一个标准C表示法表示的整数。
寄存器:表示某个寄存器内的内容。
内存引用:根据计算出来的地址访问某个的内存位置。
有多种不同的寻址方式,其中比例变址寻址比较常见,Imm(rb,ri,s),Imm表示立即数偏移,rb表示基址寄存器,ri表示变址寄存器和一个比例因子s。
数据传送指令
最频繁的指令是将数据从一个位置让复制到另一个位置的指令。
最简单形式的数据传送指令是——MOV类,这些指令把数据从源位置复制到目的位置,不做任何变化。
movb、movw、movl、movq组成了MOV类,这些指令都执行相同的操作,区别在于他们操作的数据大小不同,分别为1 2 4 8字节。
源操作数指定的值是一个立即数,存储在寄存器中或者内存中,目的操作数指定一个位置,要么是一个寄存器,要么是一个内存地址,传送指令的两个操作数不能都指向内存地址。
这些指令的寄存器数可以是16个寄存器有标号部分中的任意一个,寄存器部分的大小必须与指令最后一个字符指定的大小相匹配,MOV指令只会更新目的操作数指令的那些寄存器字节或内存位置,唯一的例外是movl指令以寄存器作为目的时,他会把该寄存器的高位4字节设置为0。
在做数据传送时,常常会遇到将较小的源传送到较大的目的,对此MOV有两类数据移动指令。
指令 | 效果 | 描述 |
---|---|---|
MOVZ S R | R 零扩展S | 以零扩展进行传送 |
movzbw | 将做了零扩展的字节传送到字 | |
movzbl | 字节传送到双字 | |
movzwl | 字传送到双字 | |
movzbq | 字节传送到四字 | |
movzwq | 字传送到四字 | |
MOVS S R | R 符号扩展S | 传送符号扩展的字节 |
movsbw | 字节传送到字 | |
movsbl | 字节传送到双字 | |
cltp | %rax——(符号扩展)%eax | 把%eax符号扩展到%rax |
如上的表省略了部分。
压入和弹出栈数据
两个数据传送操作可以将数据先压入栈中,再从栈中弹出。
pushq 压栈
popq 弹栈
将一个四字值压入栈中,首先要将栈指针减8,然后将值写到新的栈顶地址。