一、IA32处理器体系结构
1965年,Intel的创始人根据当时的芯片技术做出推断,预计在未来10年,芯片上的晶体管数量每年都会翻一番,这个预测就称为
摩尔
【Moore】
定律
。事实上,在超过50年中,半导体工业一直能够使得晶体管的数目每18个月翻一倍。
1.1 处理器体系结构
处理器体系架构
【Instruction Set Architecture,ISA】用于定义了机器级程序的格式和行为,其定义了处理器状态、指令的格式,以及每条指令对于状态的影响。
架构的具体实现称为
微架构
,包括缓存大小、核心频率等。
代码的格式分为
机器码
,为处理器直接执行的机器级程序;以及
汇编码
,是机器码的文本表示。
计算机处理器的基本结构如下:
1.2 寄存器
寄存器
是CPU内部单元的高速存储单元,访问速度比常规内存快得多。
IA32具有32位
通用寄存器
,用于算术运算和数据传输,包括:
-32位EAX,EAX的低16位AX,AX的高8位AH与低8位AL,作为累加寄存器在乘法和除法指令中被自动使用;
-32位EBX,EAX的低16位BX,AX的高8位BH与低8位BL;
-32位ECX,EAX的低16位CX,AX的高8位CH与低8位CL,作为循环计数器;
-32位EDX,EAX的低16位DX,AX的高8位DH与低8位DL;
-32位EBP,EBP的低16位BP,作为帧指针寄存器,用于引用堆栈上的函数参数和局部变量;
-32位ESP,ESP的低16位SP,作为扩展堆栈指针寄存器,用于寻址堆栈上的数据;
-32位ESI,ESI的低16位SI,作为扩展源指针寄存器,用于内存数据的存取;
-32位EDI,EDI的低16位DI,作为扩展目的指针寄存器,用于内存数据的存取;
IA32具有
段寄存器
,用于存放段的基址,包括:
-CS,用于存放代码段,即程序的指令的地址;
-DS,用于存放数据段,即程序的变量的地址;
-SS,用于存放堆栈段,即函数的局部变量和参数的地址;
-ES、FS和GS指向其他数据段。
IA32具有
指令指针寄存器
EIP,也称为
程序计数器
【Program counter,PC】,始终存放下一条要被CPU执行的指令地址。
IA32具有
标志寄存器
与
条件码寄存器
,由控制CPU的操作或反应CPU某些运算结果的二进制位构成。包括:
-OF,溢出标志,在有符号算数运算的结果无法容纳于目的操作数中时被设置;
-SF,符号标志,在算术或逻辑运算产生的结果为负时被设置;
-ZF,零标志,在算术或逻辑运算产生的结果为零时被设置:
-AF:辅助进位标志,在8位操作数的第3位到第4位产生进位时被设置;
-PF:奇偶标志,结果的最低8位中,为1的总数为偶数时被设置,并在为奇数时清除;
-CF:进位标志,在无符号算数操作的结果无法容纳于目的操作数中时被设置;
IA32具有
系统寄存器
,仅允许运行在最高特权级的程序,如操作系统内核访问的寄存器,任何应用程序禁止访问。
IA32具有浮点单元FPU,适用于高速浮点运算,包括
浮点数据寄存器
,指针寄存器和控制寄存器。
1.3 内存管理
IA32架构下的处理器在
实地址模式
下,使用20位的地址总线,访问1MB(0x0~0xFFFFF)内存。在8086架构中,只有16位的地址线,不足以表示地址,使用
段偏移地址
,将内存分为64K的段,存放在16位的段寄存器中,形成<segment:bias>的地址形式。例如地址0x80000到0x89999分别表示为0x8000:0000到0x8000:FFFF。
在
保护模式下
,操作系统使用段寄存器指向的
段描述符表
定位程序使用的段的位置,其将段映射为物理地址空间,访问代码段与数据段,并使用分页的模式实现虚拟内存。
1.4 指令执行周期
单条机器指令的执行可以分解成一系列的独立操作,这个操作序列被称为指令
执行周期
。单条指令的执行有三种基本操作:
取指
、
解码
和
执行
。程序在开始执行之前被装入内存,执行过程中,PC包含要执行的下一条指令的地址,指令队列中包含了一条或多条将要执行的指令。当CPU执行使用内存操作数的指令时,必须计算操作数的地址,将地址放在地址总线上等待存储器取出操作数。
机器指令的执行至少需要一个时钟周期。
1.5 程序运行
计算机操作系统【Operating System,OS】加载和运行程序的步骤如下:
-用户发出特定程序的命令;
-OS在当前磁盘目录查找程序文件名,如果未找到就在预定义的目录列表查找,还未找到则报出错误信息;
-OS在找到程序文件后,获取磁盘上程序文件的基本信息;
-OS确定下一个可用内存块的地址,将程序文件载入内存,将程序的信息登记在描述符表中;
-OS执行一条分支转移命令,使CPU从程序的第一条机器指令开始执行,一旦程序运行,则成为一个进程,OS为进程分配一个唯一的标识号;
-进程运行,OS跟踪进程的执行并相应进程对系统资源的要求;
-进程中止,其标识符被删除,使用的内存被释放。
OS运行的可以是一个进程或者一个执行线程,当OS能够同时运行多个任务时,被认为是多任务的,这里的同时包含着并发运行的含义。OS的调度系统为每个任务分配一小部分CPU时间片,使得多个任务之间快速切换,给人以同时运行多个任务的假象。
二、汇编语言
2.1 汇编语言
机器语言
是一种二进制语言,由0与1组成的指令代码的集合,机器能够直接识别和执行。每条指令都简单到能够用相对较少的电子电路单元即可执行。要注意的是,各种机器的指令系统互不相同。
汇编语言
的
汇编指令
采用
助记符
便于记忆与阅读,其使用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址,且汇编指令同机器指令一一对应。
高级语言与汇编语言及机器语言是一对多的关系,一条简单的C++语句会被扩展成多条汇编语言及机器语言指令。高级语言可以通过
解释
逐行转换成机器语言,或
编译
将整个程序转换成机器语言。
在汇编语言中,一些对C程序员隐藏的处理器状态是可视的,包括:
-程序计数器EIP(IA32)或RIP(x86-64),存放下一条指令的地址;
-寄存器文件,包括大量的程序数据;
-条件码,用于条件分支,存储了最近的算术或逻辑运算的状态信息;
-内存,包括程序段、数据段与堆栈段。
C包括
变量
、
运算
与
控制
,而汇编语言包括汇编指令与操作数,其数据类型仅为整型与浮点型,而没有数组、结构体等聚合类型;汇编的运算用寄存器与内存数据完成,在内存与寄存器之间传送;汇编的控制通过转移控制实现。
2.2 操作数指示符
大多数指令有一个或多个
操作数
,指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。x86-64支持多种操作数格式如下图,各种不同的操作数的可能性被分为三个类型:
–
立即数
,用来表示常数值。在ATT格式的汇编编码中,立即数的书写方式是“$”后面跟一个用标准C表示法表示的整数,如$-577,$0x1F等,其格式与操作数值形如
$
I
m
m
=
I
m
m
\$Imm = Imm
$
I
m
m
=
I
m
m
–
寄存器
,表示某个寄存器的内容,使用
r
a
r_a
r
a
来表示任意寄存器a,用引用
R
[
r
a
]
R[r_a]
R
[
r
a
]
来表示其值,其格式与操作数值形如
r
a
=
R
[
r
a
]
r_a = R[r_a]
r
a
=
R
[
r
a
]
–
内存
,其根据计算出来的地址访问某个内存位置,用符号
M
b
[
A
d
d
r
]
M_b[Addr]
M
b
[
A
d
d
r
]
表示对存储在内存中从地址
A
d
d
r
Addr
A
d
d
r
开始的b个字节值的引用,其格式与操作数值形如
I
m
m
(
r
b
,
r
i
,
s
)
=
M
[
I
m
m
+
R
[
r
b
]
+
R
[
r
i
]
⋅
s
]
Imm(r_b, r_i, s) = M[Imm+R[r_b]+R[r_i]·s]
I
m
m
(
r
b
,
r
i
,
s
)
=
M
[
I
m
m
+
R
[
r
b
]
+
R
[
r
i
]
⋅
s
]
操作数的长度通过指令标识,分为整型的1字节
b
b
b
、2字节
w
w
w
、4字节
l
l
l
、8字节
q
q
q
;浮点型的单精度s、双精度l。
2.3 数据传送
最频繁使用的指令是将数据从一个位置复制到另一个位置的指令,最简单形式的
数据传送
指令为mov,形如
movL src, dst
其中,
L
L
L
表示操作数的长度,包括8位b、16位w、32位l、64位q。mov的操作数类型可以是整型立即数、除了%rsp的整数寄存器之一以及内存,并且x86-64要求两个操作数不能都指向内存。
还有一类数据传送称为
条件传送
指令cmov,形如
cmovCL src, dst
其中,
L
L
L
表示操作数的长度,
C
C
C
表示条件,其利用标志位CF、SF、ZF、OF实现条件判断。
此外,如果源操作数位数低于目的操作数,使用
movSbl src, dst
其中,当
S
=
s
S = s
S
=
s
时,将对源操作数进行符号拓展,而
S
=
z
S = z
S
=
z
时,将对源操作数进行零拓展。例如
%rax = 0xfa4;
%rbx = 0x7645321012345678
那么使用符号扩展,有
movsbl %al, %ebx
其中%al是%rax的低8位,%ebx是%rbx的低32位,那么,%ebx的值为0xffffffa4。
2.4 算术与逻辑运算
加载有效地址【load effective address】是从内存读数据到寄存器,使用指令leaq,形如
leaq src, dst
该指令会将源操作数的地址表达式保存到目的操作数中。实际上,其可以简洁的描述普通的算术操作,例如
leaq (%rdi, %rdi, 2), %rax
其中,(%rdi, %rdi, 2)指向了M[3%rdi],而是用leaq,则将该内容的地址,即3%rdi移动到%rax中,从而简单的实现了%rax = 3%rdi。
一元操作数运算指令包括
incL dst #dst = dst + 1
decL dst #dst = dst - 1
negL dst #dst = -dst
notL dst #dst = ~dst
二元操作数运算指令包括
addL src, dst #dst = dst + src
subL src, dst #dst = dst - src
imulL src, dst #dst = dst * src
salL src, dst #dst = dst <<A src
shlL src, dst #dst = dst <<H src
sarL src, dst #dst = dst >>A src
shrL src, dst #dst = dst >>H src
xorL src, dst #dst = dst ^ src
andL src, dst #dst = dst & src
orL src, dst #dst = dst | src
2.5 浮点数
流式SIMD扩展版本3
【Streaming SIMD Extensions 3,SSE3】指令集对应了浮点体系结构,使用16个XMM寄存器,与整数型汇编指令有一定的差异。
三、控制
3.1 条件码
CPU维护着一组条件码寄存器,在算术运算的过程中隐式的赋值,例如执行
addq src, dst
的过程中,若有:
-发生了无符号溢出,取CF = 1,若未发生,则CF = 0;
-发生了有符号溢出,取OF = 1,若未发生,则OF = 0;
-发生了dst = 0,取ZF = 1,否则ZF = 0;
-发生了dst < 0,取SF = 1,否则SF = 0;
要注意的是,leaq指令不会设置条件码。
可以通过比较【compare】两个值来隐式的为条件码赋值,而不改变任何操作数,使用cmp指令,形如
cmpL src1, src2
该指令运算src1-src2,其结果会导致条件码改变,但运算结果不会被保存。
同样效果的还有通过测试【test】两个值来隐式的为条件码赋值,而不改变任何操作数,使用test指令,形如
testL src1, src2
该指令运算src1&src2,其结果会导致条件码改变,但运算结果不会被保存。
可以通过【set】访问条件码赋值,使用set指令,形如
setC dst
其会将条件码的值直接赋值给dst的低位字节,而不改变其他位。
3.2 条件分支
使用跳转【jump】来控制程序运行的进程,使用jmp指令,形如
jmp dst
jC dst
其中,jmp指令会直接跳转。跳转指令使用相对地址编码,通过跳转地址与下一顺序地址之间的差编码,考虑代码
movq %rdi, %rax
jmp .L2
.L3:
sarq %rax
.L2:
testq %rax, %rax
jg .L3
rep;ret
其.o文件的反汇编代码为
0x0: 48 89 f8
0x3: eb 03
0x5: 48 d1 f8
0x8: 48 85 c0
0xb: 7f f8
0xd: f3 c3
其中,0x3为跳转至L2的地址,其下一地址为0x5,而L2的首地址为0x8,故跳转操作数编码为
08
H
−
05
H
=
03
H
08H – 05H = 03H
0
8
H
−
0
5
H
=
0
3
H
;同样的,0xb为跳转到L3的地址,其下一地址为0xd,而L3的首地址为0x5,故跳转操作数编码为
05
H
−
0
D
H
=
F
8
H
05H – 0DH = F8H
0
5
H
−
0
D
H
=
F
8
H
。
而其链接后的反汇编代码为
0x4004d0: 48 89 f8
0x4004d3: eb 03
0x4004d5: 48 d1 f8
0x4004d8: 48 85 c0
0x4004db: 7f f8
0x4004dd: f3 c3
其跳转编码与未链接一致的,便是由于代码之间间隔不变从而使用相对地址保证了跳转数的一致。
C的goto语句与汇编的跳转控制流形式近乎相同,实现了
条件分支
。但实际上,goto被认为是一种不好的编程风格,因为其难以阅读与调试。例如
long absdiff(long x, long y){
long result;
int ntest = x <= y;
if (ntest)
goto Else;
goto Done;
Else:
result = y - x;
Done:
return rusult;
}
在上述程序中,首先判断力条件,然后在该控制下跳转到某种运算,称为
条件控制
。
另一种选择条件分支的方法称为
条件传送
,其在完成可能的运算后,再进行跳转。那么上述条件控制的代码的条件转移代码为
long absdiff(long x, long y){
long rval = y - x;
long eval = x - y;
int ntest = x <= y;
if (ntest):
rval = eval;
return rval;
}
在流水线的机制下,条件控制可能产生的分支预测及其错误可能会导致性能的严重下降,但条件传送在某些计算十分复杂时也会带来巨大的计算代价。
3.3 循环结构
汇编语言中,通过条件测试和跳转的组合实现
循环
。
while循环语句及其goto控制流可以表示为
while (test)
Body;
Done;
goto test;
loop:
Body;
test:
if (test)
goto loop;
done:
Done;
也可以表示为do-while风格的goto控制流,即
if (!test):
goto done;
loop:
Body;
if (test):
goto loop;
done:
Done;
for循环的一般形式为
for (init;test;update)
Body;
其可以转换为while风格,形如
init;
while (test){
Body;
update;
}
3.4 开关结构
C的
开关
【switch】通过判断条件测试,对其值进行跳转到相应的
情况
【case】中。其中,case包括多case,如多个值执行同一分支;下穿case,在执行该case之后继续执行下一顺序的case。
switch通过跳转表进行跳转,跳转表存放着代码地址,C的switch便可表示为
goto *Jtab[x];
其中,Jtab的元素为代码的地址。在汇编代码中,一个跳转表的例子为
.section .rodata
.align8
.L4:
.quad .L8
.quad .L3
.quad .L5
.quad .L9
.quad .L8
.quad .L7
.quad .L7
四、过程
4.1 运行时栈
C过程调用机制的一个关键特性在于使用了
栈
数据结构提供的后进先出的内存管理原则。x86-64的栈向低地址方向生长,而栈指针%rsp指向栈顶元素。栈的操作包括
压栈
【push】与
弹栈
【pop】,使用push与pop指令,形如
pushL src
popL dst
其中,push会执行压栈,首先由于栈向低地址方向生长,使得%rsp减少8,然后将src写入%rsp指向的地址;同样的,pop会执行弹栈,首先将%rsp指向的地址的内容读取到dst,此时要求dst操作数为寄存器,然后令%rsp增加8。
在x86-64过程需要的出寄存器能够存放的大小时,就会在栈上分配空间。
4.2 传递控制
在控制从函数P转移到函数Q,只需要把PC的值设置为Q代码的起始位置;但Q返回时,处理器需要记录好P继续执行的位置。
调用
【call】函数和
返回
【return】,使用call指令与ret指令,形如
call func_label
ret
在发生函数调用时,原函数的下一条指令的地址将会被压栈,直到调用的函数返回,该地址弹栈,跳转到该地址继续进行过程。
4.3 传递数据
x86-64中,可以通过寄存器最多传递6个整型,包括
-参数1,64位%rdi,32位%edi,16位%di,8位%dil;
-参数2,64位%rsi,32位%esi,16位%si,8位%sil;
-参数3,64位%rdx,32位%edx,16位%dx,8位%dl;
-参数4,64位%rcx,32位%ecx,16位%cx,8位%cl;
-参数5,64位%r8,32位%r8d,16位%r8w,8位%r8b;
-参数6,64位%r9,32位%r9d,16位%r9w,8位%r9b。
超过6个的部分通过栈来传递,要注意的是,第7个参数比第8个参数更靠近栈顶,有着更低的地址,其余参数亦是如此。这种局部变量尽在需要时才申请栈空间。局部变量的访问可以使用相对地址,例如第7个参数在C的类型为char,占有8个位,那么访问其的地址为
%
r
s
p
+
8
\%rsp + 8
%
r
s
p
+
8
。
4.4 局部储存
栈用于在从调用的发生到返回的时间内,保存特定过程的状态。
栈分配的单位称为
栈帧
,其内容包括被调用过程的返回信息、局部储存与临时空间。在进入被调用过程时,会构建栈帧,并进行call指令产生的push操作;在过程返回时会清理栈帧,并进行ret指令产生的pop操作。某个过程的栈帧空间由位于栈帧底的
帧指针
%rbp与栈帧顶的
栈指针
%rsp指定。
当前栈帧的内容从低地址到高地址依次为:
-该过程即将调用的函数所需的参数;
-该过程不能用寄存器全部储存的局部变量;
-保存的寄存器内容;
-旧栈帧指针。
考虑这样的情形,代码如下
caller:
movq $0x12, %rdx
call callee
ret
callee:
subq $0x18, %rdx
ret
此时,在caller函数中被赋值为0x18的%rdx被callee函数改写了。为此,
寄存器保存约定
需要调用者在调用前,将其在寄存器中的值保存在其栈帧中;之后,被调用者首先将寄存器中的值保存在栈帧中,然后再使用寄存器,并在返回给调用者之前恢复保存的寄存器值。
按照上述约定,寄存器的功能及其使用方式如下:
-%rax,返回值,由调用者保存,被调用者可以修改;
-%rdi,%rsi,%rdx,%rcx,%r8,%r9,传递函数参数,由调用者保存,被调用者可以修改;
-%r10,%r11,调用者保存的临时值,由调用者保存,被调用者可以修改;
-%rbx,%r12,%r13,%r14,%r15被调用者使用寄存器前保存,在返回时恢复;
-%rbp,被调用者使用寄存器前保存,在返回时恢复,或用于栈帧指针;
-%rsp,被调用者保存,在离开过程时恢复为被调用之前的值。
4.5 递归过程
递归
【recursion】是一种复杂的调用逻辑,但其过程无续特殊的处理,因为栈帧使得每个过程都有私有的储存,保存着寄存器、局部变量与返回地址;由于寄存器保存约定使得函数调用之间不会损毁其他过程的数据,除非C明确的要这样做。
典型的递归汇编的通常模式为
rfact:
movl $0, %eax
testq %rdi, %rdi
je .L6
pushq %rbx
movq %rdi, %rbx
andl $1, %ebx
shrq $1, %rdi
call rfact
addq %rbx, %rax
popq %rax
.L6:
ret
其在
%
r
d
i
≠
0
\%rdi \ne 0
%
r
d
i
=
0
的情况下持续的递归rfact函数。
五、数据
5.1 数组
C的
数组
是一种将标量数据聚集成更大数据类型的方式。对于数据类型T与整数常量N,数组的声明如下
T A[N];
其在内存中连续分配
N
∗
s
i
z
e
o
f
(
T
)
N*sizeof(T)
N
∗
s
i
z
e
o
f
(
T
)
个字节,其中标识符A作为了指向数组开头的指针。考虑访问数组
t = a[n];
其对应的汇编代码为
# %rdi = a
# %rsi = n
# %rax = t
movq (%rdi, %rsi, 4), %eax
当创建数组的数组时,即
高维数组
,数据分配和引用的一般原则也是成立的。对于一个声明如下的数组
T D[R][C];
其数组元素
D
[
i
]
[
j
]
D[i][j]
D
[
i
]
[
j
]
的内存地址为
&
D
[
i
]
[
j
]
=
D
+
s
i
z
e
o
f
(
T
)
(
C
⋅
i
+
j
)
\&D[i][j] = D + sizeof(T)(C·i + j)
&
D
[
i
]
[
j
]
=
D
+
s
i
z
e
o
f
(
T
)
(
C
⋅
i
+
j
)
考虑访问数组
int a[5][4] = {...};
x = a[n][m];
其对应的汇编代码为
# %rsi = m
# %rdi = n
# %rbx = a
# %eax = x
leaq (%rdi, %rdi, 4), %rax # %rax = 5n
addl %rax, %rsi # %rsi = 5n + m
movl (%rbx, %rsi, 4), %eax # x = M[a + 4(5n + 4)]
多层次数组
为使用指针的数组,其元素为指向数组的指针,可以和多维数组实现一样的效果。
5.2 结构体
C的
结构体
【struct】声明创建了一个数据类型,将可能不同类型的对象聚合到一个对象中。考虑如下的结构体
struct s {
int i;
int j;
int a[2];
int *p;
}
该结构包括4个字段:两个4字节的int、一个int类型的数组和一个8字节的指向int类型的指针。其字节偏移形如
要注意的是,字段顺序必须与声明一致,即使其他顺序可以使内存更紧凑,因为机器级程序不解读源代码中的结构体,而直接使用结构体成员的字节偏移。
内存一般按照4字节或8字节的对齐块访问,而对于char等仅有1个字节的数据类型,使得数据边界与内存访问不一致,可能会导致跨字数据装载的性能以及棘手的跨界面的虚拟内存。为此,一些机器要求、x86-64推荐使用
对齐
的结构体,编译器在结构体插入空白,保证字段的正确对齐。
对齐的准则为满足每个元素的对其要求,若所有元素的最大长度为K,那么起始地址与结构体长度必须是K的倍数。
考虑结构体
struct s{
char c;
int t[2];
double v;
} *p;
其最大元素为double类型,K = 8,要求数据按8对齐,其数据及地址形如
根据以上特性,一般要求大尺寸数据类型在前,使得插入的空白最少。
5.3 联合体
C的
联合体
【union】提供了一种方式规避C的类型系统,允许以多种类型来引用一个对象。其依赖最大成员申请内存,且同时只能使用一个成员。考虑如下的联合体
union u{
char c;
int i[2];
double v;
} *up
其字节偏移形如
要注意的是某个元素类型以连续的字节储存时,储存的顺序问题。
六、内存布局与溢出
6.1 内存布局
x86-64 Linux的内存布局如下
考虑如下程序
char big_array[1L << 24]; // 16MB
char huge_array[1L << 31]; // 2GB
int global = 0;
int useless(){
return 0;
}
int main(){
void *p1, *p2, *p3, *p4;
int local = 0;
p1 = malloc(1L << 28); // 256MB
p2 = malloc(1L << 8); // 256B
p3 = malloc(1L << 32); // 4GM
p4 = malloc(1L << 8); // 256B
/* Some print statement */
return 0;
}
那么有:
-local变量位于用户栈;
-p1、p2、p3、p4变量位于运行时堆;
-big_array、huge_array位于数据段;
-main()、useless()位于代码段。
6.2 缓冲区溢出
C对数组的引用不进行任何边界检查,而且局部变量和状态信息都存放在栈中,这导致对越界的数组元素的写操作会破坏储存在栈中的状态信息。一般的,当操作数据超过了为其分配的内存大小,称为
缓冲区溢出
,典型的,有字符串输入后不检查长度。
在缓冲区溢出时,若溢出的地址未使用,则不会破坏状态;但如果溢出的地址被使用,那么过程的状态,如返回地址等被破坏,会造成程序的严重错误。
考虑这样的情况,输入字符串包含可执行代码的字节序列,而将返回地址用缓冲区溢出的地址代替,从而运行了该可执行代码。这是典型的缓冲区溢出攻击,其允许远程机器在受害者机器上执行任意代码。
6.3 对抗缓冲区溢出攻击
为了防止缓冲区溢出造成的严重后果,程序员编写的代码中一定要避免溢出漏洞。
此外,系统级别提供了一定的对抗缓冲区溢出攻击的手段:
-在程序启动后,在栈中分配随机数量的空间,使整个程序使用的栈空间移动,从而使得黑客难以确定插入代码的起始地址;
-x86-64允许添加显式的执行权限,将插入代码标记为不可执行;
-使用
栈金丝雀
,在栈中缓存范围之后的位置放置特殊的值,在退出函数时检查其是否被破坏,在目前的GCC中式默认开启的。
缓冲区溢出攻击是一种
面向返回的编程攻击
【Return-Oriented Programming,ROP】,在上述策略下依然有替代的策略。