ICS03 汇编语言
type
status
date
slug
summary
tags
category
icon
password
01 从Intel x86处理器开始
在计算机科学中,机器语言是计算机硬件直接理解和执行的低级编程语言。它由二进制代码组成,直接对应于计算机的指令集架构(ISA)。本文将探讨Intel x86处理器的历史、架构、以及汇编语言的基础知识,帮助读者理解计算机硬件与软件之间的交互方式。
Intel x86架构的起点可以追溯到1978年的8086芯片,这是一款16位处理器,广泛应用于IBM PC和DOS操作系统。x86架构属于复杂指令集计算机(Complex Instruction Set Computer, CISC),其特点是指令种类繁多且格式复杂。Linux编程通常只使用其指令集的一个子集。与CISC相对的是精简指令集计算机(Reduced Instruction Set Computer, RISC),如ARM、MIPS和SPARC架构,这些架构通常具有大量寄存器,编译器承担更多工作,且功耗较低。RISC架构在80年代编译器技术提升后逐渐流行。
1986年,Intel推出了386芯片,这是一款32位处理器,采用IA32架构,支持平面地址空间(flat address space),适用于UNIX系统。2004年,Intel发布了奔腾4E处理器,采用64位x86架构,时钟频率高达2.8GHz。2006年,酷睿2处理器问世,标志着多核处理器的普及。2008年,酷睿i7处理器推出,具备四核设计,主频范围为1.6GHz至4.4GHz。
我们讨论芯片制程时,通常以纳米(nm)为单位,表示最小电路宽度。例如,10nm制程相当于100个原子的宽度。随着制程技术的进步,10nm以下的制程通常指的是等效制程。
本文的讨论范围仅限于x86-64架构。
Architecture(体系结构/架构)
体系结构,也称为指令集架构(Instruction Set Architecture, ISA),是处理器设计的一部分,定义了指令集规范、寄存器等硬件与软件之间的接口。ISA为编写机器语言和汇编语言提供了便利。ISA通常具有向后兼容性,即新型CPU可以支持旧的ISA。
形象地来说,ISA是计算机科学工作者与集成电路工作者间沟通的桥梁。双方共同定下ISA,规定处理器等硬件的指令集,也即应该支持什么样的机器指令。在ISA之上,计算机工作者基于这些机器指令,开发编译程序,将高级语言编译成由机器指令组成的机器语言;开发操作系统,充当应用软件与底层硬件间的沟通者和协调者;开发计算机网络,将多台计算机连接起来。在此基础上,软件工程从业者进行应用软件的开发。而这几个部分也可以粗略地对应于CS专业本科生的“四大礼包”,即:计算机体系结构、编译原理、操作系统和计算机网络。在ISA之下,集成电路工作者设计并制造承载着处理器等硬件的芯片。
Micro Architecture(微架构)
微架构是指处理器内部如何实现ISA的具体硬件设计,包括缓存大小、核心主频等细节。
CPU与内存
处理器(CPU)和内存(Memory)是计算机系统中的两个关键组件。程序或操作序列存储在内存中,CPU负责执行这些指令。CPU的主要组件包括:
- 程序计数器(PC):存储下一条指令的地址,也称为指令指针(RIP)。
- 寄存器堆(Register File):很小、很快,用于存储频繁使用的数据。
- 条件码(Condition Codes):存储最近执行的算术或逻辑操作的结果。
对于初学者而言,内存是一个以字节为单位的随机访问序列,用于存储代码、指令、数据和栈。CPU通过地址访问内存,并从中读取数据或指令。
编译
广义的编译是将高级语言代码(例如C语言)转换为机器语言的过程。具体步骤如下:
- 使用文本编辑器编写C语言源文件(.c)。
- 通过预处理器(如cpp)展开C源文件中的
include
,替换define
等宏定义,形成预处理文件(.i)。
- 通过编译器(如gcc)将C文件编译为汇编文件(.s)。
- 通过汇编器将汇编文件转换为目标文件(.o,二进制文件)。
- 通过链接器将目标文件链接为可执行文件(二进制文件)。
x86-64汇编语言
数据类型
在汇编语言中,数据类型主要分为整数和浮点数。整数数据可以占用1、2、4或8个字节,浮点数数据可以占用4、8或16个字节。汇编语言中不再使用数组,而是通过指针进行操作。
指令操作
汇编语言中的指令操作主要包括以下几类:
- 在内存和寄存器之间传递数据。
- 对寄存器或内存中的数据进行算术操作。
- 控制流转换,包括无条件跳转、条件分支与转移、以及非直接转移。
x86架构的指令长度是可变的,占用1至15个字节。例如,C语言代码
*dest = t;
表示将数据t
存储到dest
指向的内存区域,对应的汇编指令为movq %rax, (%rbx)
,其中rax
寄存器存储数据,rbx
寄存器存储目标内存地址。整数寄存器
x86架构中的整数寄存器包括:
rax
(累加器)
rbx
(基址寄存器)
rcx
(计数器)
rdx
(数据寄存器)
rsi
(源索引寄存器)
rdi
(目标索引寄存器)
rbp
(基址指针)
rsp
(栈指针):通常不直接使用
r8
至r15
(扩展寄存器)汇编语法
在x86汇编语言中,指令后缀表示操作数的大小:
q
表示四字(quadword,8字节,64位)
l
表示双字(longword,4字节,32位)
w
表示字(word,2字节,16位)
b
表示字节(byte,1字节,8位)
大多数情况下,x86机器上的指令加
q
与不加q
没有区别。但有一个例外:当mov
指令的一个操作数是立即数,另一个是内存地址时,必须使用q
后缀。移动值指令 movq
- 语法:
movq src, dest
src
可以是立即数、寄存器地址或内存地址。
mov
操作可以是立即数到寄存器、立即数到内存、寄存器到寄存器、寄存器到内存、或内存到寄存器。
- 内存到内存的操作必须通过寄存器进行。
注意:movl指令会将寄存器的高32位自动置零。
不同的汇编器支持不同的汇编语法风格,其中最常见的是 Intel 语法 和 AT&T 语法(也称为 Linux 语法)。
Intel 语法
- 由 Intel 公司设计,广泛用于 Windows 和 DOS 环境。
- 语法简洁,操作数的顺序是
目标操作数, 源操作数
。
- 寄存器名称和立即数没有前缀或后缀。
- 内存地址用
[ ]
表示。
- 示例:
AT&T 语法(Linux 语法)
- 由 AT&T 公司设计,广泛用于 Unix/Linux 系统(如 GCC 内联汇编)。
- 操作数的顺序是
源操作数, 目标操作数
。
- 寄存器名称前加
%
,立即数前加$
。
- 内存地址用
()
表示。
- 指令通常带有操作数大小的后缀(如
b
表示字节,w
表示字,l
表示双字)。
- 示例:
AT&T 语法常用于 Linux 环境(如 GCC 内联汇编),与 Unix/Linux 工具链集成紧密。在CSAPP和本系列文章中,均使用AT&T/Linux语法。
加载有效地址指令 leaq
leaq
(load effective address in quadword)指令用于进行地址计算。- 语法:
leaq src, dest
src
指定要计算的地址(可以是地址表达式),dest
指定存储结果的寄存器。
- 地址表达式的格式为:
D(Rb, Ri, S) = Mem[Reg[Rb] + S * Reg[Ri] + D]
其中,
Rb
是基址寄存器,Ri
是变址寄存器,S
是比例因子(只能是1、2、4或8),D
是偏移量。
Mem[x]
表示从内存的地址x处取值;Reg[r]
表示从标号为r的寄存器中取值,也即r中存放的值。leaq
指令还可以用于算术运算。例如,leaq (%rax, %rax, 2), %rcx
将%rax
的值乘以3并存储到%rcx
中。注意:leaq指令不会改变条件码。
其他汇编指令
双操作数指令:
单操作数指令:
一个数除以2的n次方,可以等价于算术右移n位。然而,算术右移视为除法运算时是向下取整的,而除法运算的规则是向零取整。因此,对于负数
a
除以k=2^n
的操作,应等价于(a + 2^n - 1) >> n
。乘法指令
imulq
指令支持多种形式,具体如下:- 单操作数形式:
imulq SRC
,将RAX
寄存器的值与SRC
相乘,结果存储在RDX:RAX
中。
- 双操作数形式:
imulq SRC, DEST
,将SRC
与DEST
相乘,结果存储在DEST
中。
- 三操作数形式:
imulq IMM, SRC, DEST
,将立即数IMM
与SRC
相乘,结果存储在DEST
中。
imulq适用于有符号整数乘法,而mulq适用于无符号整数乘法。
02 控制流
在计算机科学中,控制流是指程序执行指令的顺序。理解控制流对于掌握计算机如何执行条件操作和循环至关重要。本文将探讨汇编语言中的控制流机制,包括条件码、控制指令的使用,以及现代指令集架构中的条件传送技术。
条件码/标志位(Conditional Code/Flag Bits)
条件码或标志位是一组状态信息,用于存储最近一次算术或逻辑操作的结果,这些结果用于条件跳转指令中的判断。条件码包含以下比特:
- ZF(Zero):零标志,表示运算结果是否为0。
- OF(Overflow):溢出标志,表示运算是否溢出(针对有符号数)。
- SF(Sign):符号标志,表示结果是否为负(针对有符号数),实际上可用最高位表示。
- CF(Carry):进位标志,表示结果是否产生进位或借位(针对无符号数溢出)。
使用条件码+控制指令完成比较
cmpq src2, src1
:计算src1 - src2
,结果不保存到任何寄存器,但会改变条件码的FLAG。
testq src2, src1
:计算两个寄存器中内容的按位与,同样只改变条件码。test src, src
:ZF为1当且仅当src为0,等价于src == 0
。- 实际上这与
andq src, src
的效果相同。
sete reg
:将reg寄存器的最低位置成ZF的值(是否为零/equal)。
setne
:置成~ZF
。
sets
:置成SF
(是否为负)。
setns
:置成~SF
。
setg
(greater):置成~(SF^OF)&~ZF
(非零,且符号标志等于溢出标志)。
setge
(greater/equal):置成~(SF^OF)
。
setl
(less):置成SF^OF
。
setle
(less/equal):置成ZF|(SF^OF)
。
- 无符号数:
seta
、setb
(above/below)。
- 常见的目标:
%al
指rax寄存器的最低字节,%eax
指rax寄存器的低4字节。
movzbl
(move with zero-extend, byte to long):将一个字节移动到一个32位长整型寄存器中(在64位系统中也会将前面四个字节也清零)。
使用控制指令完成分支
je/jz
- jump if equal/zero
jne
- jump if not equal
- 对于有符号数:
jg
- jump if greaterjge
- jump if greater or equaljl
- jump if lessjle
- jump if less or equal
- 对于无符号数:
ja
- jump if abovejb
- jump if below
jmp
- unconditional jump
jump系指令
在机器语言中,
0x70 ~ 0x7F
是jump系指令,共十六个,分别对应e/ne, g/ge, l/le, a/ae, b/be, SF/nSF, OF/nOF, p/np。jump系指令的操作数是要跳转到的指令所在的地址;在机器语言中以相对位置(偏移)表示。例如:- 汇编语言中有
jmp label
。
- 机器语言中为:
- 对应规则:
dest = next_addr + bias
。next_addr
即为PC/RIP的内容。
寻址模式 | 语法示例 | 描述 |
直接寻址 | jmp label | 跳转到代码中的指定标签(绝对地址)。这种模式是最常见的。 |
寄存器间接寻址 | jmp *%rax | 跳转到存储在寄存器(如 %rax )中的地址。地址可以动态改变,用于灵活控制流程。 |
内存间接寻址跳转 | jmp *8(%rax) | 跳转到寄存器指向的内存中存放的地址。 |
现代ISA:Conditional Move(条件传送)
1995年后的x86处理器新增了新的指令
cmove
,其功能为if(test) dest <- src
。GCC编译器在保证安全的前提下优先使用该指令。在流水线式CPU的工作中,条件传送指令能够显著提升效率。传统架构中,分支判断需要耗费时钟周期,线程必须等待判断完成,这会导致时间开销的增加。为了解决这一问题,CPU采用了分支预测技术,提前选择一条分支进行执行。然而,预测错误时,流水线会被清空并重新填充,这一过程需要多个时钟周期(详见第四章,处理器体系结构)。相比之下,新式架构通过条件传送指令避免了跳转操作。具体而言,在判断之前,CPU会同时执行两条分支的指令,最终根据条件结果对结果进行覆盖。
这种方法减少了流水线中断的可能性,但可能会导致不安全的情况。例如在表达式
p == NULL ? *p : 0
中,可能会对NULL
指针进行解引用;或者在表达式(*a > *b) ? ++(*a) : (*b)++
中,可能会对全局变量a
进行不必要的修改。因此,编译器在使用条件传送指令时,会优先保证代码的安全性。需要注意的是,条件传送指令不能传送单字节数据,并且并不总是能提高效率,特别是在两侧分支的操作非常复杂的情况下。从本质上讲,条件传送指令的执行可以理解为同时运行两侧的代码。
使用控制指令完成循环
循环结构(如
while
、do-while
和for
)可以转换为goto
语句,并在汇编语言中通过跳转指令实现。例如,do { A } while (B);
可以转换为:类似地,
for
循环也可以采用相同的方式进行转换。对于
switch
语句,编译器的处理方式取决于分支的数量和分布。当分支较少且分布稀疏时,编译器通常会将其转换为多个if-else
语句。而在分支较多的情况下,编译器会采用跳转表(Jump Table)的方式来实现。跳转表是一个包含各个代码块首地址的顺序数组。在执行
switch
语句时,根据表达式的值计算出跳转表的偏移量,从而直接跳转到相应的代码块。这种机制类似于哈希表,但在 case 分支分布较为分散时,效率会有所下降。03 工作过程
在现代计算机体系结构中,函数调用是程序执行的核心机制之一。为了实现函数调用、返回以及管理局部变量等复杂功能,计算机体系设计者引入了栈(Stack)这一数据结构,同时规定了一些标准的接口规范用于编译器、操作系统和硬件之间的协作。
函数/过程的执行主要包括以下三个重要要素:传递控制、传递数据以及内存管理。在函数调用时,控制的传递通过指令跳转完成;数据的传递通常依赖于寄存器的约定;而内存管理则涉及栈的分配与回收。
应用二进制接口(ABI)
Application Binary Interface(ABI)是一个标准化的软件接口,定义了应用程序与操作系统之间的低级接口细节,使得不同的软件组件能够互相协作。ABI的核心内容包括:
- 函数调用约定
- 二进制格式
- 系统调用接口
- 数据类型
- 寄存器的使用
ABI与硬件体系结构(ISA)不同,它并非是指导硬件设计的规范,而是编译器、链接器以及操作系统在编译和运行时需要遵循的标准。ABI的定义位于更高的抽象层次,与具体的硬件无关。
x86-64中的栈
在x86-64架构中,栈(Stack)的内存管理遵循一定的规范。栈通常从高地址向低地址增长,栈顶为当前最小的内存地址,其值存储在
%rsp
寄存器(Stack Pointer)中。栈的操作
push
指令 将数据压入栈顶时,%rsp
寄存器的值自动减少64位(8字节),然后将数据写入新的栈顶地址。
popq
指令 从栈顶弹出数据时,先将栈顶地址中的值存入目标寄存器,随后将%rsp
寄存器的值增加64位。
通过栈实现函数调用与返回
函数调用:
call
指令- 将
call
指令的下一条指令地址(即返回地址)压入栈中。
- 修改
%rsp
寄存器的值以指向新的栈顶。
- 跳转至目标函数的入口地址。
函数返回:
ret
指令- 从栈中弹出返回地址,将其存储在
%rip
(指令指针)寄存器中。
- 下一周期执行
%rip
所指向的指令。
参数与返回值传递
根据x86-64的ABI约定:
- 前六个参数通过如下寄存器传递:
%rdi
、%rsi
、%rdx
、%rcx
、%r8
、%r9
。
- 其余参数存储在栈中,从低到高依次排列。
- 返回值存储在
%rax
寄存器中。
历史背景
最初,Intel的架构设计使用栈来传递函数参数和返回值。这种设计源于寄存器资源的限制(仅8个通用寄存器)。随着硬件技术的进步,内存访问的时间成本增加,Intel改为通过寄存器传递参数和返回值,同时扩展了寄存器的数量。
函数调用中的栈帧管理
局部变量处理
主流编程语言(如C、Pascal、Java)通常采用基于栈的内存管理模式,以支持递归。这要求函数调用是“可重进入的”(Reentrant)。每次函数调用会在栈上创建一个栈帧,用于存储局部变量、函数参数和返回地址。
栈帧的基址由寄存器
%rbp
(Base Pointer)指向。在函数调用开始时:- 将原始基指针
%rbp
压入栈中。
- 更新
%rbp
为当前栈顶。
- 根据需要分配栈空间存放局部变量。
栈帧的生命周期
- 进入函数
push %rbp
:保存原始基指针到栈顶。movq %rsp, %rbp
:将栈顶地址赋值给基指针。subq 16, %rsp
:分配所需栈空间,这里是16字节
- 离开函数
addq 16, %rsp
:释放分配的栈空间。movq %rbp, %rsp
:将栈指针还原到栈帧基址。pop %rbp
:恢复调用者的基指针。ret
:利用栈顶存储的返回地址跳转至调用者代码。
总结
通过栈的使用,x86-64架构的函数调用与返回机制实现了对控制流和数据流的高效管理。ABI所规范的寄存器使用策略,使得函数间的数据传递更加高效,同时降低了内存操作的开销。在理解这些机制的基础上,程序员可以更好地优化代码性能,同时避免栈溢出等潜在问题。
04 数据
在计算机体系结构和编程中,数据的存储和操作是核心环节之一。无论是数组、结构体、联合体还是浮点数,它们的存储形式和访问方式都直接影响程序的性能和内存使用效率。
数组
在高级语言中,数组在编译后存储于内存中,形成连续的字节序列。数组的基本形式为
T a[L]
,总共占用L * sizeof(T)
字节。指针数组的长度取决于指针的大小。C语言中访问数组的方式包括:a[i]
: 访问元素值,索引从0开始。
a + i
: 计算元素地址,实际计算为a + i * sizeof(T)
。
*(a + i)
: 取得元素值,与a[i]
等价。
变量类型
a
是大小为L的T型数组,数组名可隐式转换为指向数组首元素的T型指针。
&a
为指向大小为L的T型数组的指针,类型为T (*)[L]
。
汇编语言
在汇编中,以4字节int数据为例,数组访问示例如下:
movl (%rdi,%rsi,4), %eax
其中,
%rdi
存放数组起始地址,%rsi
存放访问索引,每个元素宽度为4字节,mov
指令将对应索引的数值存放到目标寄存器%eax
中。多维数组
多维数组
T A[R][C]
在内存中连续存放,占用R * C * sizeof(T)
字节。A[i]
是一个数组,A[i][j]
是一个元素,其地址为A + (i * C + j) * sizeof(T)
。结构体
结构体的元素在内存中的排列顺序与声明时一致,编译器通过地址和偏移量来访问结构体成员。
结构体对齐
- 基础类型占用k字节的结构体元素,其地址偏移量为k的整数倍。
- 结构体起始地址是所有元素最大size的整数倍,结构体的总大小也是最大元素size的整数倍。
- 对齐提高了访存效率,但可能导致内存浪费,称为“内存碎片”(内部为internal padding,外部为external padding)。
- C语言头部使用
#pragma pack(n)
可设定对齐参数,n取1, 2, 4或8。
例如我们声明一个结构体:
假设一个
T
型元素的起始地址是0x128
,则其元素的分布为:联合体
联合体允许多种不同类型的数据共享同一内存空间。示例:
联合体的大小取决于最大成员的size,对齐方式也依照最大成员。
浮点数
浮点数的发展经历了x87、SSE和AVX阶段,利用SIMD(Single Instruction Multiple Data)理念来提高效率。
浮点数寄存器
以XMM寄存器为例,每个寄存器大小为16字节,编号为
%xmm0
到%xmm15
。这些寄存器可视为多个单精度或双精度浮点数,并行操作。浮点数操作
常见指令包括:
- 单精度标量加法:
addss %xmm0, %xmm1
- 单精度矢量加法:
addps %xmm0, %xmm1
- 双精度标量加法:
addsd %xmm0, %xmm1
指令后缀描述:
后缀&全称 | 描述 |
SS
Scalar Single-Precision | 对单个单精度浮点数的标量操作,其他位保持不变。 |
PS
Packed Single-Precision | 对 4 个(SSE 128 位)或 8 个(AVX 256 位)单精度浮点数进行并行操作。 |
PD
Packed Double-Precision | 对 2 个(SSE 128 位)或 4 个(AVX 256 位)双精度浮点数进行并行操作。 |
SD
Scalar Double-Precision | 对单个双精度浮点数的标量操作,其他位保持不变。 |
05 安全问题
缓冲区溢出
如果在调用某函数时,函数访问了高于其栈帧顶的位置,那么会造成缓冲区溢出。严重的情况是写入了父函数栈帧尾存放的返回地址,导致本函数结束时跳转到完全陌生位置。利用这一特性,攻击者可以将攻击代码注入到栈中,然后写入返回地址,使其指向攻击代码。蠕虫(常被误称为病毒)常使用此方式攻击。
常见的缓冲区溢出:
- 读写数组时不检查索引
- 写入字符串时不检查长度。例如
stdin
中的gets
函数,在读入字符串并顺序写入到参数指针中不会检查是否越界
常见的解决方法:
- 将攻击代码被注入的位置保密,使攻击者无法找到。例如将栈的起始位置随机化(randomized)。然而,攻击者可以使用nop sled滑行到攻击代码,也即插入大量的
nop
指令(其功能只是简单地跳转到下一条指令继续执行)。
- 函数返回时检查缓冲区是否被侵入。常见的方法是在栈帧中放置金丝雀值(canary/guard)。然而攻击者可以通过某种机制逐个字节猜出金丝雀值。作为防御,可以每次重新运行时更改金丝雀值。
金丝雀值的机制
金丝雀值(也称为栈保护值)是一种用于检测和防止栈溢出攻击的安全机制。它的名称来源于矿工用金丝雀检测矿井中有毒气体的做法——如果金丝雀死亡,矿工就知道有危险。类似地,金丝雀值用于检测栈是否被破坏。
工作原理:
- 插入金丝雀值:在函数调用时,编译器会在栈帧中插入一个随机的金丝雀值,通常位于返回地址之前。这个值在程序运行时是随机生成的,攻击者难以预测。
- 检查金丝雀值:在函数返回之前,编译器会插入代码来检查金丝雀值是否被修改。如果金丝雀值被修改(例如由于栈溢出攻击),程序会立即终止,防止攻击者利用栈溢出执行恶意代码。
- 防止栈溢出攻击:栈溢出攻击通常通过覆盖返回地址来控制程序执行流。由于金丝雀值位于返回地址之前,攻击者必须覆盖金丝雀值才能修改返回地址。如果金丝雀值被覆盖,程序会检测到并终止,从而阻止攻击。
实际应用:
- 现代编译器(如 GCC 和 Clang)默认启用了栈保护机制(如
fstack-protector
),在函数中插入金丝雀值以增强安全性。
- 将内存划分为可读、可写、可执行部分,令栈区的数据为不可执行。但攻击者可以进行ROP攻击,无需定位攻击函数被插入的位置;不会惊动金丝雀值;跳转到的内存区块是可执行的。事实上,Attack Lab的最后一部分正是基于此原理设置的。
Return-Oriented Programming
ROP(Return-Oriented Programming)是一种高级的攻击技术,用于绕过现代操作系统的内存保护机制,如 NX(No-Execute)位 或 DEP(Data Execution Prevention)。这些机制将内存划分为可读、可写和可执行部分,通常将栈区标记为不可执行,以防止攻击者在栈中注入并执行恶意代码。然而,ROP 攻击通过利用已有的代码片段(称为 gadgets)来绕过这些保护。
攻击原理:
- 栈不可执行:现代操作系统将栈标记为不可执行,攻击者无法直接在栈中插入并执行恶意代码。
- 利用
ret
指令:攻击者通过栈溢出或其他漏洞覆盖栈上的返回地址。当函数返回时,ret
指令会将栈顶的值弹出并赋值给RIP
(指令指针寄存器),从而控制程序执行流。
- 构造 ROP 链:攻击者在栈上构造一个 ROP 链,即一系列指向已有代码片段(gadgets)的地址。每个 gadget 通常以
ret
指令结尾,攻击者通过连续调用这些 gadgets 来实现复杂的逻辑。例如,攻击者可以将RIP
设置为库函数的地址,并传递参数来执行任意命令。
- 利用库函数:攻击者将
RIP
设置为库中的某个小函数(如system
、execve
等)的地址。通过在栈上布置参数,攻击者可以调用这些函数来执行任意操作。
防御措施:
- ASLR(Address Space Layout Randomization):
- 随机化内存布局,增加攻击者定位 gadgets 和库函数地址的难度。
- CFI(Control Flow Integrity):
- 通过检查控制流是否合法,防止攻击者跳转到非预期的代码位置。
- ROP 检测工具:
- 使用工具检测程序中的 ROP gadgets,并采取措施消除或减少其可用性。
总结:x86-64 Linux内存规则
在x86-64 Linux系统中,内存布局如下:
附录:C语言中的类型命名规则
在C语言中,变量名的类型可通过以下规则判定:
- 括号内先读后面:
[]
为数组,()
为函数。再看前面:*
为指针。
- 再看括号外
示例:
附录:C语言中的sizeof
- 指针大小恒定为8字节。
- 数组大小为所有元素大小之和。
示例:
Prev
ICS02 数据
Next
ICS04 处理器体系结构
Loading...