机器级编程的抽象
指令集体系结构
定义了处理器状态、指令的格式、每条指令对状态的影响
大多数ISA将程序的行为描述成好像每条指令是按顺序执行的,一条指令结束后,下一条再开始
处理器的硬件远比描述的精细复杂,它们并发执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致
虚拟地址
虚拟地址使得存储器模型看上去是一个非常大的字节数组,将多个硬件存储器和操作系统软件组合起来
汇编语言
循环
大多数汇编器根据一个循环的do-while
形式来产生循环代码,其他循环会首先转换成do-while
形式,然后再编译成机器代码
do-while
do {
body-statement;
} while (test-expr);
// 翻译成goto
loop:
body-statement;
t = text-expr;
if (t)
goto loop;
int fact_do (int n) {
int result = 1;
do {
result *= n;
n = n - 1;
} while (n > 1);
return result;
}
movl 8(%ebp), %edx # get n
movl $1, %eax # set result = 1
.L2: # loop:
imull %edx, %ax # compute result *= n
subl $1, %edx # decrement n
cmpl $1, %edx # compare n: 1
jg .L2 # if >, goto loop
# return result
while
while (test-expr) {
body-statement;
}
// 翻译成goto
t = test-expr
if (!t)
goto done;
loop:
body-statement;
t = test-expr;
if (t)
goto loop;
done:
int fact_while (int n) {
int result = 1;
while (n > 1) {
result *= n;
n = n - 1;
}
return result;
}
movl 8(%ebp), %edx # get n
movl $1, %eax # set result = 1
cmpl $1, %edx # compare n: 1
jle .L7 # if <=, goto done
.L10: # loop:
imull %edx, %eax # compute result *= n
subl $1, %edx # decrement n
cmpl $1, %edx # compare n: 1
jg .L10 # if >, goto loop
.L7: # done
# return result
for
for (init-expr; test-expr; update-expr) {
body-statement;
}
// 翻译成goto
init-expr
t = test-expr;
if (!t)
goto done;
loop:
body-statement;
update-expr;
t = test-expr;
if (t)
goto loop;
done:
int fact_for (int n) {
int i;
int result = 1;
for (i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
movl 8(%ebp), %ecx # get n
movl $2, %edx # set i to 2
movl $1, %eax # set result = 1
cmpl $1, %ecx # compare n: 1
jle .L14 # if <=, goto done
.L17: # loop:
imull %edx, %eax # compute result *= i
addl $1, %edx # increment i
cmpl %edx, %ecx # compare n: i
jge .L17 # if >=, goto loop
.L14: # done
# return result
条件操作
控制的条件转移
机制简单通用
流水线上效率低
数据的条件转移
先计算一个条件操作的两种结果,然后根据条件是否满足从而选取一个
一般来说会改进代码效率
但是当分支需要大量计算,当相对应的条件不满足,工作白费
给GCC以命令行选项-march=i686
开启
int absdiff (int x, int y) {
return x < y ? y-x : x-y;
}
int cmovdiff (int x, int y) {
int tval = y - x;
int rval = x - y;
int test = x < y;
if (test)
rval = tval;
return rval;
}
movl 8(%ebp), %ecx # get x
movl 12(%ebp), %edx # get y
movl %edx, %ebx # copy y
subl %ecx, %ebx # compute y-x
movl %ecx, %eax # copy x
subl %edx, $eax # compute x-y and set as return value
cmpl %edx, %ecx # compare x: y
cmovl %ebx, %eax # if <, replace return value with y-x
switch
可以根据一个整数索引值进行多重分支
处理具有多种可能结果的测试时,这种语句特别有用,不仅提高了C代码可读性,而且使用跳转表这种数据结构使得实现更加高效
跳转表是一个数组,表相i时代码段的地址,这个代码段实现相当于开关索引值等于i时程序应该采取的动作
程序代码用开关索引值来执行一个跳转表内的数组饮用,确定跳转指令的目标
过程
一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部份。另外还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间
使用栈保存局部变量的原因
- 没有足够多的寄存器存放所有的局部变量
- 有些局部变量是数组或结构,必须通过数组或结构饮用来访问
- 对一个局部变量使用地址操作符
&
,必须能够为它生成一个地址
寄存器使用惯例
- 调用者保存寄存器:
%eax
,%edx
,%ecx
- 被调用者保存寄存器:
%ebx
,%esi
,%edi
- 必须保存寄存器:
%ebp
,%esp
数据对齐
许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值
K
K
(2、4、8)的倍数。这种对齐限制简化了行程处理器和存储器系统之间接口的硬件设计。SSE单元和存储器之间传送数据的指令要求存储器地址必须是16的倍数,栈帧的长度都是16字节的倍数(返回地址,保存的%esp
分别占4字节)
Linux对齐策略
- 2字节数据类型(short)的地址必须是2的倍数
- 较大的数据类型(int、void*、float、double)的地址必须是4的倍数
Windows对齐策略
任何字节基本对象的地址都必须是 K K (2、4、8)的倍数
- double和long double数据类型的地址必须是8的倍数
存储器越界
数组引用不进行边界检查,而且局部变量和状态信息都存放在栈里,当程序破坏状态,试图重新加载寄存器或使用ret
指令就会出现严重错误
缓冲区溢出
gets 和 fgets
gets(char*)
函数不进行边界检查,会导致存储溢出fgets(char*, int, FILE*)
函数进行边界检查,不会导致存储溢出
缓冲区溢出攻击
缓冲区溢出更加致命的使用就是执行本来不愿意执行的函数,这是最常见的通过计算机网络攻击系统安全的方法
一种攻击形式,攻击代码会使用系统调用启动一个外壳程序,给攻击者提供一组操作系统函数
另一种攻击形式是,攻击代码会执行一些未授权的人物,修复对栈的破坏,然后第二次执行ret
指令,表面上正常返回给调用者
对抗缓冲区溢出攻击
栈随机化
使得栈的位置在程序每次运行时都有变化。Linux使用地址空间布局随机化(ASLR),每次运行时程序的不同部分,包括程序代码、库代码、栈、全局变量、堆数据都会加载到存储器不同区域
栈破坏检测
最近的GCC版本在产生的代码中加入了一种栈保护者机制,用来检测缓冲区越界。其思想是在栈帧种人和局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值,在程序每次运行时随机产生。在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个操作改变了,如果是,程序异常终止
使用命令行选项
-fno-stack-protector
来阻止GCC产生这种代码限制可执行代码区域
一种方法是限制那些能够存放可执行代码的存储器区域。在典型的程序中,只有保存编译器产生的代码的那部分存储器才需要是可执行的,其它部分可以被限制为只允许读和写
x86-64
特性
数据类型
- 指针从32位变成64位
- 长整型(long)从32位变成64位
访问信息
- 寄存器数量从8个翻倍到16个
- 寄存器都是64位长,IA32寄存器的64位扩展分别命名为
%rax
,%rcx
,%rdx
,%rbx
,%rsi
,%rdi
,%rsp
,%rbp
。新增的寄存器命名为%r8
,%r9
,%r10
,%r11
,%r12
,%r13
,%r14
,%r15
- 可以直接访问每个寄存器的低32位:
%eax
,%ecx
,%edx
,%ebx
,%esi
,%edi
,%esp
,%ebp
,%r8d
,%r9d
,%r10d
,%r11d
,%r12d
,%r13d
,%r14d
,%r15d
- 可以直接访问每个寄存器的低16位:
%ax
,%cx
,%dx
,%bx
,%si
,%di
,%sp
,%bp
,%r8w
,%r9w
,%r10w
,%r11w
,%r12w
,%r13w
,%r14w
,%r15w
- 可以直接访问每个寄存器的低8位:
%al
,%cl
,%dl
,%bl
,%sil
,%dil
,%spl
,%bpl
,%r8b
,%r9b
,%r10b
,%r11b
,%r12b
,%r13b
,%r14b
,%r15b
%rsp
有特殊的状态,会保存指向栈顶元素的指针- 没有帧指针寄存器
- 可以用
%rbp
作为通用寄存器 - 基址寄存器和变址寄存器必须使用寄存器的
r
版本
算术指令
- 增加了在四字上进行运算的指令,后缀为
q
控制
- 增加了在四字上进行比较合测试的指令,后缀为
q
过程
- 参数(最多前六个参数)可以通过寄存器传递到过程(
%rdi
,%rsi
,%rdx
,%rcx
,%r8
,%r9
),而不是在栈上,消除了在栈上存储和检索值的开销 callq
指令将一个64位返回地址存储在栈上- 许多函数不需要栈帧,只有那些不能将所有局部变量都存放在寄存器的函数才需要在栈上分配空间
- 函数最多可以访问超过当前栈指针值128个字节的栈上存储空间
- 没有帧指针,作为代替,对栈位置的引用相对于栈指针
栈帧
需要栈的唯一原因就是用来保存返回地址
可能需要栈帧的原因
- 局部变量太多,不能都放在寄存器
- 有些局部变量是数组或结构
- 函数用取地址操作符来计算一个局部变量的地址
- 函数必须将栈上的某些参数传递到另一个函数
- 在修改一个被调用者保存寄存器之前,函数需要保存它的状态
数据结构
- 数组作为同样大小的块的序列来分配(同IA32一样)
- 结构作为变长的块来分配(同IA32一样)
- 联合作为一个单独的块来分配,这个块足够大能够装下联合中最大的元素(同IA32一样)
- 对于任何需要字节的标量数据类型来说,它的起始地址必须是 K K <script type="math/tex" id="MathJax-Element-93">K</script>的整数倍