Lazy loaded imageICS03 汇编语言

type
status
date
slug
summary
tags
category
icon
password

01 从Intel x86处理器开始

在计算机科学中,机器语言是计算机硬件直接理解和执行的低级编程语言。它由二进制代码组成,直接对应于计算机的指令集架构(ISA)。本文将探讨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语言)转换为机器语言的过程。具体步骤如下:
  1. 使用文本编辑器编写C语言源文件(.c)。
  1. 通过预处理器(如cpp)展开C源文件中的include ,替换define等宏定义,形成预处理文件(.i)。
  1. 通过编译器(如gcc)将C文件编译为汇编文件(.s)。
  1. 通过汇编器将汇编文件转换为目标文件(.o,二进制文件)。
  1. 通过链接器将目标文件链接为可执行文件(二进制文件)。

x86-64汇编语言

数据类型

在汇编语言中,数据类型主要分为整数和浮点数。整数数据可以占用1、2、4或8个字节,浮点数数据可以占用4、8或16个字节。汇编语言中不再使用数组,而是通过指针进行操作。

指令操作

汇编语言中的指令操作主要包括以下几类:
  1. 在内存和寄存器之间传递数据。
  1. 对寄存器或内存中的数据进行算术操作。
  1. 控制流转换,包括无条件跳转、条件分支与转移、以及非直接转移。
x86架构的指令长度是可变的,占用1至15个字节。例如,C语言代码*dest = t;表示将数据t存储到dest指向的内存区域,对应的汇编指令为movq %rax, (%rbx),其中rax寄存器存储数据,rbx寄存器存储目标内存地址。

整数寄存器

x86架构中的整数寄存器包括:
rax(累加器) rbx(基址寄存器) rcx(计数器) rdx(数据寄存器) rsi(源索引寄存器) rdi(目标索引寄存器) rbp(基址指针) rsp(栈指针):通常不直接使用 r8r15(扩展寄存器)

汇编语法

在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操作可以是立即数到寄存器、立即数到内存、寄存器到寄存器、寄存器到内存、或内存到寄存器。
  • 内存到内存的操作必须通过寄存器进行。
 
page icon
注意:movl指令会将寄存器的高32位自动置零。
 
page icon
不同的汇编器支持不同的汇编语法风格,其中最常见的是 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中。
       
      page icon
      注意:leaq指令不会改变条件码。
       

      其他汇编指令

      双操作数指令
      单操作数指令
       
      page icon
      一个数除以2的n次方,可以等价于算术右移n位。然而,算术右移视为除法运算时是向下取整的,而除法运算的规则是向零取整。因此,对于负数a除以k=2^n的操作,应等价于(a + 2^n - 1) >> n
       
      乘法指令 imulq指令支持多种形式,具体如下:
      1. 单操作数形式imulq SRC,将RAX寄存器的值与SRC相乘,结果存储在RDX:RAX中。
      1. 双操作数形式imulq SRC, DEST,将SRCDEST相乘,结果存储在DEST中。
      1. 三操作数形式imulq IMM, SRC, DEST,将立即数IMMSRC相乘,结果存储在DEST中。
      imulq适用于有符号整数乘法,而mulq适用于无符号整数乘法。

      02 控制流

      page icon
      在计算机科学中,控制流是指程序执行指令的顺序。理解控制流对于掌握计算机如何执行条件操作和循环至关重要。本文将探讨汇编语言中的控制流机制,包括条件码、控制指令的使用,以及现代指令集架构中的条件传送技术。

      条件码/标志位(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)
      • 无符号数:setasetb(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 greater
        • jge - jump if greater or equal
        • jl - jump if less
        • jle - jump if less or equal
      • 对于无符号数:
        • ja - jump if above
        • jb - 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 + biasnext_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进行不必要的修改。因此,编译器在使用条件传送指令时,会优先保证代码的安全性。
        需要注意的是,条件传送指令不能传送单字节数据,并且并不总是能提高效率,特别是在两侧分支的操作非常复杂的情况下。从本质上讲,条件传送指令的执行可以理解为同时运行两侧的代码。

        使用控制指令完成循环

        循环结构(如whiledo-whilefor)可以转换为goto语句,并在汇编语言中通过跳转指令实现。例如,do { A } while (B);可以转换为:
        类似地,for循环也可以采用相同的方式进行转换。
        对于switch语句,编译器的处理方式取决于分支的数量和分布。当分支较少且分布稀疏时,编译器通常会将其转换为多个if-else语句。而在分支较多的情况下,编译器会采用跳转表(Jump Table)的方式来实现。
        跳转表是一个包含各个代码块首地址的顺序数组。在执行switch语句时,根据表达式的值计算出跳转表的偏移量,从而直接跳转到相应的代码块。这种机制类似于哈希表,但在 case 分支分布较为分散时,效率会有所下降。

        03 工作过程

        page icon
        在现代计算机体系结构中,函数调用是程序执行的核心机制之一。为了实现函数调用、返回以及管理局部变量等复杂功能,计算机体系设计者引入了栈(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)指向。在函数调用开始时:
        1. 将原始基指针%rbp压入栈中。
        1. 更新%rbp为当前栈顶。
        1. 根据需要分配栈空间存放局部变量。

        栈帧的生命周期

        1. 进入函数
            • push %rbp:保存原始基指针到栈顶。
            • movq %rsp, %rbp:将栈顶地址赋值给基指针。
            • subq 16, %rsp:分配所需栈空间,这里是16字节
        1. 离开函数
            • addq 16, %rsp:释放分配的栈空间。
            • movq %rbp, %rsp:将栈指针还原到栈帧基址。
            • pop %rbp:恢复调用者的基指针。
            • ret:利用栈顶存储的返回地址跳转至调用者代码。

        总结

        通过栈的使用,x86-64架构的函数调用与返回机制实现了对控制流和数据流的高效管理。ABI所规范的寄存器使用策略,使得函数间的数据传递更加高效,同时降低了内存操作的开销。在理解这些机制的基础上,程序员可以更好地优化代码性能,同时避免栈溢出等潜在问题。

        04 数据

        page icon
        在计算机体系结构和编程中,数据的存储和操作是核心环节之一。无论是数组、结构体、联合体还是浮点数,它们的存储形式和访问方式都直接影响程序的性能和内存使用效率。

        数组

        在高级语言中,数组在编译后存储于内存中,形成连续的字节序列。数组的基本形式为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函数,在读入字符串并顺序写入到参数指针中不会检查是否越界

        常见的解决方法:

        1. 将攻击代码被注入的位置保密,使攻击者无法找到。例如将栈的起始位置随机化(randomized)。然而,攻击者可以使用nop sled滑行到攻击代码,也即插入大量的nop 指令(其功能只是简单地跳转到下一条指令继续执行)。
        1. 函数返回时检查缓冲区是否被侵入。常见的方法是在栈帧中放置金丝雀值(canary/guard)。然而攻击者可以通过某种机制逐个字节猜出金丝雀值。作为防御,可以每次重新运行时更改金丝雀值。
        page icon

        金丝雀值的机制

        金丝雀值(也称为栈保护值)是一种用于检测和防止栈溢出攻击的安全机制。它的名称来源于矿工用金丝雀检测矿井中有毒气体的做法——如果金丝雀死亡,矿工就知道有危险。类似地,金丝雀值用于检测栈是否被破坏。

        工作原理:

        1. 插入金丝雀值:在函数调用时,编译器会在栈帧中插入一个随机的金丝雀值,通常位于返回地址之前。这个值在程序运行时是随机生成的,攻击者难以预测。
        1. 检查金丝雀值:在函数返回之前,编译器会插入代码来检查金丝雀值是否被修改。如果金丝雀值被修改(例如由于栈溢出攻击),程序会立即终止,防止攻击者利用栈溢出执行恶意代码。
        1. 防止栈溢出攻击:栈溢出攻击通常通过覆盖返回地址来控制程序执行流。由于金丝雀值位于返回地址之前,攻击者必须覆盖金丝雀值才能修改返回地址。如果金丝雀值被覆盖,程序会检测到并终止,从而阻止攻击。

        实际应用:

        • 现代编译器(如 GCC 和 Clang)默认启用了栈保护机制(如 fstack-protector),在函数中插入金丝雀值以增强安全性。
        1. 将内存划分为可读、可写、可执行部分,令栈区的数据为不可执行。但攻击者可以进行ROP攻击,无需定位攻击函数被插入的位置;不会惊动金丝雀值;跳转到的内存区块是可执行的。事实上,Attack Lab的最后一部分正是基于此原理设置的。
         
        page icon

        Return-Oriented Programming

        ROP(Return-Oriented Programming)是一种高级的攻击技术,用于绕过现代操作系统的内存保护机制,如 NX(No-Execute)位DEP(Data Execution Prevention)。这些机制将内存划分为可读、可写和可执行部分,通常将栈区标记为不可执行,以防止攻击者在栈中注入并执行恶意代码。然而,ROP 攻击通过利用已有的代码片段(称为 gadgets)来绕过这些保护。

        攻击原理:

        1. 栈不可执行:现代操作系统将栈标记为不可执行,攻击者无法直接在栈中插入并执行恶意代码。
        1. 利用 ret 指令:攻击者通过栈溢出或其他漏洞覆盖栈上的返回地址。当函数返回时,ret指令会将栈顶的值弹出并赋值给 RIP(指令指针寄存器),从而控制程序执行流。
        1. 构造 ROP 链:攻击者在栈上构造一个 ROP 链,即一系列指向已有代码片段(gadgets)的地址。每个 gadget 通常以 ret 指令结尾,攻击者通过连续调用这些 gadgets 来实现复杂的逻辑。例如,攻击者可以将 RIP 设置为库函数的地址,并传递参数来执行任意命令。
        1. 利用库函数:攻击者将 RIP 设置为库中的某个小函数(如 systemexecve 等)的地址。通过在栈上布置参数,攻击者可以调用这些函数来执行任意操作。

        防御措施:

        1. ASLR(Address Space Layout Randomization)
            • 随机化内存布局,增加攻击者定位 gadgets 和库函数地址的难度。
        1. CFI(Control Flow Integrity)
            • 通过检查控制流是否合法,防止攻击者跳转到非预期的代码位置。
        1. ROP 检测工具
            • 使用工具检测程序中的 ROP gadgets,并采取措施消除或减少其可用性。

        总结:x86-64 Linux内存规则

        在x86-64 Linux系统中,内存布局如下:

        附录:C语言中的类型命名规则

        在C语言中,变量名的类型可通过以下规则判定:
        • 括号内先读后面:[]为数组,()为函数。再看前面:*为指针。
        • 再看括号外
        示例:

        附录:C语言中的sizeof

        • 指针大小恒定为8字节。
        • 数组大小为所有元素大小之和。
        示例:
         
         
         
        Prev
        ICS02 数据
        Next
        ICS04 处理器体系结构
        Loading...
        Article List
        SunVapor的小站
        计算机系统导论
        文档