计算机系统——程序的机器级表示

  • Post author:
  • Post category:其他



一、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】,在上述策略下依然有替代的策略。



版权声明:本文为X1009190387原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。