CSAPP-03程序的机器级表示
计算机执行机器代码,用字节序列编码低级的操作,包括处理数据、管理内存、读写存储设备上的数据,以及利用网络通信。
编译器基于编程语言的规则、目标机器的指令集和操作系统遵循的惯例,经过一系列的阶段生成机器代码。
GCC C语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。然后GCC调用汇编器和链接器,根据汇编代码生成可执行的机器代码。
汇编代码:
- 通过汇编代码了解程序的实际运行和效率;
- 通过汇编代码了解并发程序如何共享数据或保持数据私有;
- 通过汇编代码了解程序漏洞及防御攻击。
精通细节是理解更深和更基本概念的先决条件。
主要内容:
- C语言、汇编代码以及机器代码之间的关系;
- x86-64的细节,数据的表示和处理以及控制的实现(if、while、switch);
- 过程的实现:维护一个运行栈来支持过程间数据和控制的传递,以及局部变量的存储;
- 在机器级如何实现数组、结构和联合这样的数据结构;
- 内存访问越界问题,缓冲区溢出攻击问题;
- GDB调试器检查机器级程序运行时行为;
- 浮点数据和操作的代码的机器程序表示。
计算机工业已经完成从32位到64位机器的过度:
- 32位机器只能使用大概4GB的随机访问存储器;
- 64位机器能够使用256TB的内存空间,而且很容易扩展至16EB。
每个后续处理器的设计都是向后兼容的:较早版本上编译的代码可以在较新的处理器上运行。为了保持这种进化传统,指令集中有许多非常奇怪的东西。
程序编码
抽象:
- 指令集体系结构(Instruction Set Architecture, ISA):定义机器级抽象的格式和行为。它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
- 虚拟内存地址:提供的内存模型看上去是一个非常大的字节数组。操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器内存中的物理地址。
处理器状态:
- 程序计数器PC:%rip 给出将要执行的下一条指令在内存中的地址;
- 整数寄存器文件:包含16个命名位置,分别存储64位的值,这些寄存器可以存储地址或整数数据;
- 条件码寄存器:保存着最近执行的算术或逻辑指令的状态信息,用来实现控制或数据流中的条件变化,比如if和while语句;
- 一组向量寄存器:可以存放一个或多个整数或浮点数值。
代码示例
main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
void mulstore(long, long, long*);
int main()
{
long d;
mulstore(2, 3, &d);
printf("2 * 3 --> %ld \n", d);
return 0;
}
long mult2(long a, long b){
long s = a * b;
return s;
}
mstore.c
1
2
3
4
5
6
long mult2(long, long);
void multstore(long x, long y, long *dest) {
long t = mult2(x, y);
*dest = t;
}
使用 “-S” 编译产生汇编代码:
1
linux> gcc -Og -S main.c mstore.c
汇编文件mstore.s
.file "mstore.c"
.text
.globl multstore
.type multstore, @function
multstore:
.LFB0:
.cfi_startproc
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movq %rdx, %rbx
call mult2@PLT
movq %rax, (%rbx)
popq %rbx
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE0:
.size multstore, .-multstore
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
其中以 “.” 开头的行都是指导汇编器和链接器工作的伪指令。核心指令如下:
multstore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
使用 “-c” 编译并汇编该代码:
1
linux> gcc -Og -c mstore.c
目标代码mstore.o,二进制格式的,共1368字节,其中有一段14字节的序列:
1
53 48 89 D3 E8 00 00 00 00 48 89 03 5B C3
这就是上面列出的汇编指令对应的目标代码。
从中得到一个重要信息,即机器执行的程序只是一个字节序列,它是对一系列指令的编码,机器对产生这些指令的源代码几乎一无所知。
反汇编:
1
linux> objdump -d mstore.o
结果:
1
2
3
4
5
6
7
8
9
mstore.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <multstore>:
0: 53 push %rbx
1: 48 89 d3 mov %rdx,%rbx
4: e8 00 00 00 00 callq 9 <multstore+0x9>
9: 48 89 03 mov %rax,(%rbx)
c: 5b pop %rbx
d: c3 retq
数据格式
C语言数据类型在 x86-64 中的大小,在64位机器中,指针长8字节。
C 声明 | Intel数据类型 | 汇编代码后缀 | 大小(字节) |
---|---|---|---|
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
大多数GCC生成的汇编代码指令都有一个字符的后缀,表明操作数的大小。
例如,数据传送指令有4个变种:
- movb:传送字节;
- movw:传送字;
- movl:传送双字;
- movq:传送四字。
注意:汇编代码使用后缀 “l” 来表示4字节整数和8字节双精度浮点数,这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。
访问信息
一个x86-64的CPU包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针。
名字都以%r开头,不过后面跟着一些由于指令集历史演化造成不同的命名规则的名字。
- 最初8086中有8个16位寄存器,%ax到%bp。
- IA32架构时,寄存器也扩展成32位寄存器,%eax到%ebp。
- x86-64后,原来的8个寄存器扩展成64位,%rax到%rbp。还增加了8个新的寄存器,%r8到%r15。
指令可以对这16个寄存器的低位字节中存放的不同大小的数据进行操作。
- 字节级操作可以访问最低的字节;
- 16位操作可以访问最低的2个字节;
- 32位操作可以访问最低的4个字节;
- 64位操作可以访问整个寄存器,8字节。
对于生成小于8字节结果的指令,寄存器中剩下的字节会怎么样,对此有两条规则:
- 生成1字节和2字节数字的指令会保持剩下的字节不变;
- 生成4字节数字的指令会把高位4个字节置为0。
操作数指示符
三种类型:
- 立即数(immediate):常数值;
- 寄存器(register):某个寄存器的内容;
- 内存引用:根据计算出来的地址(通常称为有效地址)访问某个内存位置。有多种不同的寻址模式,允许不同形式的内存引用。
- 绝对寻址;
- 间接寻址;
- (基址+偏移量)寻址;
- 变址寻址;
- 比例变址寻址。Imm(r_b, r_i, s)表示的是最常用的形式。
数据传送指令
最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。
操作数表示的通用性使得一条简单的数据传送指令能够完成在许多机器中要好几条不同指令才能完成的功能。
- 操作数
- 源操作数
- 立即数
- 寄存器
- 内存
- 目的操作数
- 寄存器
- 内存
- 源操作数
x86-64加了一条限制:传送指令的两个操作数不能都指向内存位置。将一个值从一个内存位置复制到另一个内存位置需要两个指令,内存位置1 -> 寄存器 -> 内存位置2。
指令类:
- MOV 类:
- movl例外:以寄存器作为目的时,会把该寄存器的高位4字节设置为0。
- movq:常规的movq指令只能以表示为32位补码数字的立即数作为源操作数,然后把这个值符号扩展得到64位的值,放到目的位置。
- movabsq:能够以任意64位立即数值作为源操作数,并且只能以寄存器作为目的。
- MOVZ 类:将较小的源值复制到较大的目的。
- 零扩展(Zero-extending):把目的中剩余的字节填充为0;
- MOVS 类:将较小的源值复制到较大的目的。
- 符号扩展(Sign-extending):通过符号扩展来填充。
压入和弹出栈数据
%rsp保存着栈顶元素的地址。
算术和逻辑操作
这些指令只有leaq没有其他大小的变种,其他指令都有不同大小操作数的变种(b、w、l、q)。
被分为四组:
- 加载有效地址
- 一元操作
- 二元操作
- 移位
加载有效地址
leaq实际上是movq指令的变形,是从内存读取数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。这条指令可以为后面的内存引用产生指针。
另外,它还可以简洁地描述普通地算术操作(加法和有限的乘法):
1
2
3
4
long scale(long x, long y, long z){
long t = x + 4 * y + 12 * z;
return t;
}
汇编代码(书上):
// long scale(long x, long y, long z)
// x in %rdi, y in %rsi, z in %rdx
scale:
leaq (%rdi,%rsi,4), %rax // x + 4* y
leaq (%rdx,%rdx,2), %rdx // z + 2*z = 3*z
leaq (%rax,%rdx,4), %rax // (x+4*y) + 4*(3*z) = x + 4*y + 12*z;
ret
汇编代码(本机测试):
// long scale(long x, long y, long z)
// x in %rdi, y in %rsi, z in %rdx
scale:
leaq (%rdi,%rsi,4), %rax // x + 4* y
leaq (%rdx,%rdx,2), %rcx // z + 2*z = 3*z
leaq 0(,%rcx,4), %rdx // 4*(3*z)
addq %rdx, %rax // (x+4*y) + 4*(3*z) = x + 4*y + 12*z;
ret
不能直接乘以12是因为:比例因子取值只能是【1,2,4,8】中的一个。
一元和二元操作
一元操作:只有一个操作数,既是源又是目的。如:i++ 和 i–。可以是寄存器、内存位置。
二元操作:加、减、乘、异或、或、与。
移位操作
左移、算术右移、逻辑右移。
例子:
1
2
3
4
5
6
7
long arith(long x, long y, long z){
long t1 = x ^ y;
long t2 = z * 48;
long t3 = t1 & 0x0F0F0F0F;
long t4 = t2 - t3;
return t4;
}
汇编代码:
// long arith(long x, long y, long z)
// x in %rdi, y in %rsi, z in %rdx
arith:
xorq %rsi, %rdi // t1 = x ^ y
leaq (%rdx,%rdx,2), %rdx // 3 * z
movq %rdx, %rax
salq $4, %rax // t2 = 16 * (3*z) = 48z
andl $252645135, %edi // t3 = t1 & 0x0F0F0F0F
subq %rdi, %rax // return t2 - t3
ret
其中,用移位操作优化了乘法:
48z = 16 * (3 * z) = (3 * z) « 4,左移4位相当于乘以16。
特殊的算术操作
两个64位有符号和无符号整数相乘得到的乘积需要128位来表示。
控制
机器代码提供两种基本的低级机制来实现有条件的行为:
- 测试数据值,然后根据测试的结果来改变控制流或者数据流。
条件码
除了整数寄存器,CPU还维护着一组单个位的条件码(condition code)寄存器,它们描述了最近的算术或逻辑操作的属性。可以检测这些寄存器来执行条件分支指令。最常用的条件码有:
- CF:进位标志。
- ZF:零标志。
- SF:符号标志。
- OF:溢出标志。
访问条件码
条件码通常不会直接读取,常用的使用方法有三种:
- 可以根据条件码的某种组合,将一个字节设置为0或者1;
- 可以条件跳转到程序的某个其他的部分;
- 可以有条件地传送数据。
跳转指令
跳转指令的编码:
- PC相对的(PC-relative):将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差作为编码。
- ”绝对“地址:用4个字节直接指定目标。
条件控制实现条件分支
条件传送实现条件分支
条件控制实现条件分支这种机制简单通用,但是可能会非常低效。
一种替代的策略是使用数据的条件转移。这种方法计数一个条件操作的两种结果,然后再根据条件是否满足从中选取一个。只有在一些受限制的情况中,这种策略才可行,但是如果可行,就可以用一条简单的条件传送指令来实现它,条件传送指令更符合现代处理器的性能特性。
这和处理器通过使用流水线(pipelining)来获得高性能有关,通过重叠连续指令的步骤来获得高性能。
循环
do-while循环
1
2
3
4
5
6
7
8
9
10
11
// do-while 形式
do
body-statement
while(test-expr);
// 条件和goto语句形式
loop:
body-statement
t = test-expr;
if(t)
goto loop;
while循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// while 语句的通用形式
while (test-expr)
body-statement
// 第一种翻译方法:跳转到中间(jump to middle)
goto test;
loop:
body-statement
test:
t = test-expr;
if (t)
goto loop;
// 第二种翻译方法:guarded-do
t = test-expr;
if (!t)
goto done;
do
body-statement
while (test-expr);
done:
// 相应地,进一步可以翻译成gogo代码
t = test-expr;
if (!t)
goto done;
loop:
body-statement
t = test-expr;
if (t)
goto loop;
done:
for循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// for 循环的通用形式
for (init-expr; test-expr; update-expr)
body-statement
// 除了一个例外(continue),这样的循环和如下while循环一样:
init-expr;
while (test-expr){
body-statement
update-expr;
}
// 跳转到中间策略
init-expr;
goto test;
loop:
body-statement
update-expr;
test:
t = test-expr;
if (t)
goto loop;
// guarded-do策略
init-expr;
t = test-expr;
if(!t)
goto done;
loop:
body-statement
update-expr;
t = test-expr;
if (t)
goto loop;
done:
// 例外:continue如何处理?
switch语句
switch语句的关键是通过跳转表来访问代码位置。
switch例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void switch_eg(long x, long n, long *dest)
{
long val = x;
switch (n){
case 100:
val *= 13;
break;
case 102:
val += 10;
/* Fall through*/
case 103:
val += 11;
break;
case 104:
case 106:
val *= val;
break;
default:
val = 0;
}
*dest = val;
}
汇编:
switch_eg:
subq $100, %rsi
cmpq $6, %rsi
ja .L8
leaq .L4(%rip), %rcx
movslq (%rcx,%rsi,4), %rax
addq %rcx, %rax
jmp *%rax
.L4: // 跳转表
.long .L3-.L4
.long .L8-.L4
.long .L5-.L4
.long .L6-.L4
.long .L7-.L4
.long .L8-.L4
.long .L7-.L4
.text
.L3:
leaq (%rdi,%rdi,2), %rax
leaq (%rdi,%rax,4), %rdi
jmp .L2
.L5:
addq $10, %rdi
.L6:
addq $11, %rdi
.L2:
movq %rdi, (%rdx)
ret
.L7:
imulq %rdi, %rdi
jmp .L2
.L8:
movl $0, %edi
jmp .L2
.cfi_endproc
过程
抽象。提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现某种功能,然后,可以在程序中不同的地方调用这个函数。
不同语言中,过程的形式多样:函数(function)、方法(method)、子例程(subroutine)、处理函数(handler)等等。
假设过程P调用过程Q,Q执行后返回到P:
- 传递控制:在进入Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面的那条指令的地址。
- 传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
- 分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间。
运行时栈
转移控制
call + ret指令:
call Q // 过程调用,指明被调用过程起始的指令地址(会把“返回地址”A压入栈中,即紧跟在call指令后面那条指令的地址,并将PC设置为Q的起始地址)
ret // 从过程调用中返回,会从栈中弹出地址A,并把PC设置为A
数据传送
寄存器+栈。
x86-64中,大部分过程间的数据传送是通过寄存器实现的。如:Q的返回值放到寄存器%rax中,P即可访问。
如果函数整型参数个数超出6个,就要通过栈来传递。
如下,由8个参数:
1
2
3
4
5
6
7
8
9
10
void proc(long a1, long *a1p,
int a2, int *a2p,
short a3, short *a3p,
char a4, char *a4p)
{
*a1p += a1;
*a2p += a2;
*a3p += a3;
*a4p += a4;
}
汇编:参数1-6通过寄存器传递,而参数7-8通过栈传递。
/*
a1 in %rdi (64 bits)
a1p in %rsi (64 bits)
a2 in %edx (32 bits)
a2p in %rcx (64 bits)
a3 in %r8w (16 bits)
a3p in %r9 (64 bits)
a4 at %rsp+8 (8b its)
a4p at %rsp+16 (64 bits)
*/
proc:
movq 16(%rsp), %rax
addq %rdi, (%rsi)
addl %edx, (%rcx)
addw %r8w, (%r9)
movl 8(%rsp), %edx
addb %dl, (%rax)
ret
栈上的局部存储
局部数据存放在内存中:
- 寄存器不足够存放所有的本地数据。
- 对一个局部变量使用地址运算符“&”,因此必须能够为它产生一个地址。
- 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。
寄存器中的局部存储空间
寄存器组是唯一被所有过程共享的资源。
虽然在给定时刻只有一个过程是活动的,仍然必须确保当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器值。
根据惯例,寄存器%rbx、%rbp和%r12~%r15被划分为被调用者保存寄存器。当过程P调用过程Q时,Q必须保存这些寄存器的值,保证它们的值在Q返回到P时与Q被调用时是一样的。
过程Q要么根本不去改变它,要么就是把原始值压入栈中,改变寄存器的值然后返回前从栈中弹出旧值。
所有其他的寄存器,除了栈指针%rsp,都分类为调用者保存寄存器。任何函数都能修改它们。调用之前首先保存好这个数据是P(调用者)的责任。
递归过程
1
2
3
4
5
6
7
8
9
10
11
long rfact(long n)
{
long result;
if (n <= 1){
result = 1;
}
else{
result = n * rfact(n - 1);
}
return result;
}
汇编:
// long rfact(long n)
// n in %rdi
rfact:
cmpq $1, %rdi
jg .L8
movl $1, %eax
ret
.L8:
pushq %rbx
movq %rdi, %rbx
leaq -1(%rdi), %rdi
call rfact
imulq %rbx, %rax
popq %rbx
ret
每个过程调用在栈中都有它自己的私有空间,因此多个未完成调用的局部变量不会相互影响。
数组分配和访问
指针运算
嵌套的数组
定长数组
C语言编译器能够优化定长多维数组上的操作代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define N 160
typedef int fix_matrix[N][N];
/**
* 计算矩阵A和B乘积,即A的行i和B的列k的内积。
*/
int fix_prod_ele (fix_matrix A, fix_matrix B, long i, long k)
{
long j;
int result = 0;
for(j = 0; j < N; j++){
result += A[i][j] * B[j][k];
}
return result;
}
汇编:gcc -O1 -S array_fix_prod_ele.c 使用-O1优化。
fix_prod_ele:
leaq (%rdx,%rdx,4), %rax
salq $7, %rax
addq %rax, %rdi
leaq (%rsi,%rcx,4), %rdx
leaq 102400(%rdx), %rsi
movl $0, %eax
.L2:
movl (%rdi), %ecx
imull (%rdx), %ecx
addl %ecx, %eax
addq $4, %rdi
addq $640, %rdx
cmpq %rsi, %rdx
jne .L2
rep ret
汇编优化后的指令对应如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int fix_prod_ele_opt (fix_matrix A, fix_matrix B, long i, long k)
{
int *Aptr = &A[i][0]; // A的第i行0列,即i行起始地址。
int *Bptr = &B[0][k]; // B的第0行k列,即k列起始地址。
int *Bend = &B[N][k]; // B的第N行k列,即k列结束地址。
int result = 0;
do{
result += *Aptr * *Bptr; // 取地址值相乘
Aptr ++;
Bptr += N;
}while (Bptr != Bend);
return result;
}
变长数组
历史上,C语言只支持大小在编译时就能确定的多维数组(对第一维可能有些例外)。使用变长数组时不得不用malloc或calloc这样的函数为这些数组分配存储空间,而且不得不显示地编码,用行优先索引将多维数组映射到一维数组。
ISOC99引入了一种功能,运行数组地维度是表达式,在数组被分配的时候才计算出来。
动态的版本必须用乘法指令对i伸缩n倍,而不能用一系列的移位和加法。
在一个循环中引用变长数组时,编译器常常可以利用访问模式的规律来优化索引的计算。
异质的数据结构
- struct:将多个对象集合到一个单位中;
- union:允许几种不同的类型来引用一个对象。
结构
类似于数组的实现,结构的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段(field)的字节偏移。它以这些偏移作为内存引用指令中的位移,从而产生对结构元素的引用。
联合
联合提供了一种方式,能够规避C语言的类型系统,允许以多种类型来引用一个对象。
一个联合的总的大小等于它最大字段的大小。
数据对齐
许多计算机系统对于基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(2、4、8)的倍数。这种对齐限制简化了形成处理器和内存系统之间接口的硬件设计。
控制与数据结合
理解指针
指针是C语言的一个核心特色。它们以一种统一的方式,对不同数据结构中的元素产生引用。
- 每个指针都对应一个类型;
- 每个指针都有一个值;
- 指针用 ‘&’ 运算符创建;
- ‘*’ 操作符用于间接引用指针;
- 数组与指针紧密联系;
- 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值;
- 指针也可以指向函数。
内存越界和缓冲区溢出
C对于数组引用不进行任何边界检查,而且局部变量和状态信息(例如保存的寄存器值和返回地址)都存放在栈中。这两种情况结合到一起就能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息。
缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法。通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码(exploit code),另外,还有一些字节会用一个指向攻击代码的指针覆盖返回地址,那么,执行ret指令的效果就是跳转到攻击代码。
- 在一种攻击形式中,攻击代码会使用系统调用启动一个shell程序,给攻击者提供一组操作系统函数。
- 在另一种攻击形式中,攻击代码会执行一些未授权的任务,修复对栈的破坏,然后第二次执行ret指令,(表面上)正常返回到调用者。
对抗缓冲区溢出攻击
- 栈随机化:使得栈的位置在程序每次运行时都有变化。Linux系统中栈随机化已经变成了标准行为。
- 栈破坏检测:栈保护者机制。在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值,也成为哨兵值(guard value),程序每次运行时随机产生。攻击者没有简单的办法能够知道它是什么,在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个函数的某个操作改变了,如果是那么程序异常中止。
- 限制可执行代码区域:消除攻击者向系统中插入可执行代码的能力。限制哪些内存区域能够存放可执行代码。
支持变长栈帧
有些函数,需要的局部存储是变长的。例如,当函数调用 alloca 时就会发生这种情况。alloca 可以在栈上分配任意字节数量的存储。当代码声明一个局部变长数组时,也会发生这种情况。
为了管理变长栈帧,x86-64代码使用寄存器%rbp作为帧指针(frame pointer),有时称为基指针(base pointer)。
浮点代码
浮点体系结构:
- 如何存储和访问浮点数值。通常是通过某种寄存器方式来完成。
- 对浮点数操作的指令。
- 向函数传递浮点数参数和从函数返回浮点数结构的规则。
- 函数调用过程中保存寄存器的规则。
媒体(media)指令,支持图形和图像处理。本意是允许多个操作以并行模式执行,称为单指令多数据或SIMD。
媒体寄存器: