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

一、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 $Imm=Imm  -寄存器,表示某个寄存器的内容,使用 r a r_a ra来表示任意寄存器a,用引用 R [ r a ] R[r_a] R[ra]来表示其值,其格式与操作数值形如 r a = R [ r a ] r_a = R[r_a] ra=R[ra]  -内存,其根据计算出来的地址访问某个内存位置,用符号 M b [ A d d r ] M_b[Addr] Mb[Addr]表示对存储在内存中从地址 A d d r Addr Addr开始的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] Imm(rb,ri,s)=M[Imm+R[rb]+R[ri]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 08H05H=03H;同样的,0xb为跳转到L3的地址,其下一地址为0xd,而L3的首地址为0x5,故跳转操作数编码为 05 H − 0 D H = F 8 H 05H - 0DH = F8H 05H0DH=F8H
  而其链接后的反汇编代码为

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 %rsp+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 %rdi=0的情况下持续的递归rfact函数。


五、数据

5.1 数组
  C的数组是一种将标量数据聚集成更大数据类型的方式。对于数据类型T与整数常量N,数组的声明如下

T A[N];

其在内存中连续分配 N ∗ s i z e o f ( T ) N*sizeof(T) Nsizeof(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+sizeof(T)(Ci+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】,在上述策略下依然有替代的策略。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值