CTF汇编基础

CTF竞赛权威指南之汇编基础的学习

0x1 CUP架构与指令集

0x1.1 指令集架构

  • 最先诞生的是复杂指令集计算机(CISC),典型代表就是x86处理器。

  • 后面1974年IBM提出了 精简指令计算机(RISC) 的概念。旨在通过减少指令的数量和简化指令的格式来优化和提高CPU的指令执行效率。典型代表是ARM处理器。

两种指令集的对比

  • 大多数RISC的指令长度是固定的,对于32位的ARM处理器,所有指令都是4个字节,即32位;而CISC的指令长度是不固定的,通常在1到6个字节之间。固定长度的指令有利于解码和优化,可以实现流水线,缺点则是平均代码长度更大,会占用更多的存储空间。
  • 基于80%的工作由其中20%的指令完成的原则,RISC设计的指令数量相对较少,或者说更简洁。
  • 对于寻址方式,由于ARM采用了load/store架构,处理器的运算指令在执行过程中只能处理立即数,或者寄存器中的数据,而不能访问内存。因此,存储器和寄存器之间的数据交互,由专门的load(加载)和store(回存)指令负责。相反,X86既能处理寄存器中的数据,也能处理存储器中的数据,因此寻址方式更加多样,通常可以分为立即寻址**(例如mov eax ,0),寄存寻址(mov eax, ebx),直接寻址(mov eax , [0x200abd])和寄存器间接寻址(mov eax ,[ebx])**
  • 指令数量的限制使得RISC处理器需要更多的通用寄存器。ARM通常包含31个通用寄存器,而X86只有8个(EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP),x86-64则可以增加到16个(R8 ~ R15)。寄存器数量的差异在函数调用的设计上尤为明显,RISC可以完全使用寄存器来传递参数,而CISC只能完全使用栈(x86),或者结合使用栈和部分寄存器(x86-64)。

0x2 x86/x64汇编基础

0x2.1语法风格

x86汇编语言的主要语法风格有两种:AT&T风格和Intel风格。

  • intel公司设计了x86架构,设计了intel风格的汇编语言。
  • AT&T公司的前身是贝尔实验室,这是C语言和GUN Linux的诞生地。实验室的开发者希望汇编语言的语法有更好的可移植性,于是他们设计了AT&T风格的汇编语言,这类语法风格在Linux下有着广泛的支持,GCC,GDB和objdump等工具都默认使用AT&T风格。
AT&T语法风格Intel语法风格
寄存器前加%符号寄存器前无符号
立即数前加$符号立即数前无符号
16进制数使用0x前缀16进制数使用h后缀
源操作数在前,目标操作数在后目标操作数在前,源操作数在后
间接寻址用()表示间接寻址用[]表示
操作位数为指令+1,w,b(如0x11)操作位数为指令+dword ptr等(如QWORD PTR[RAX]
间接寻址格式为%sreg:disp(%base,index,scale)间接寻址格式:sreg:[basereg + index *scale +disp]

0x2.2 寄存器

在64位模式下,操作数默认大小仍然为32位,且有8个通用寄存器;当给每条指令增加REX(寄存器扩展)的前缀后,操作数变为64位,且增加了8个带有标号的通用寄存器(R8~R15)

此外,64位处理器还有两个不容忽视的特点:

  • 第一,64位与32位有着相同的标志位状态;
  • 第二,64位模式下不能访问通用寄存器的高位字节(如AH,BH,CH及DH)

0x2.3 数据传送与访问

  • MOV指令是最基本的数据传送指令,几乎所有的程序都有使用,甚至有研究者证明了MOV指令是图灵完备MOV指令能够将寄存器的值传送到寄存器的值,也可以将寄存器的值传送到内存,但不能从内存传送到内存。

  • XCHG允许我们交换两个操作数的值,可以是寄存器到寄存器,也可以是内存到寄存器,或者寄存器到内存。

  • x86汇编语言使用变量名+偏移量表示一个直接偏移量操作数。

0x2.4 算术运算与逻辑运算

  • 最简单的算术运算指令是INC和DEC,分别用于操作数加一和操作数减一。这两条指令的操作数既可以是寄存器,也可以是内存
  • 计算机底层的数据表示均是以补码表示的。
  • ADD指令将长度相同的操作数进行相加操作。
  • SUB指令为减法操作,将从目的操作数中减去源操作数。
  • 在汇编语言中存在标志位寄存器,使用SUB,ADD等指令都可能会造成整数溢出,符号位等标志位发生变化,因此进位标志位,零标志位,符号标志位,溢出标志位,辅助标志位和奇偶标志位都将根据存入的输入发生变化。
  • NEG指令是把操作数转换为二进制补码,并将操作数的符号位取反。

0x2.5 跳转指令和循环指令

  • 一般情况下,CPU是顺序加载并执行程序的,但是,指令集中会存在一些条件型指令,将根据CPU的标志位寄存器决定程序控制流的走向。
  • JMP指令是无条件跳转指令,在编写汇编语言时需要使用一个标号来标识,汇编器在编译时就会将该标号转换为相应的偏移量。一般情况下,该标号必须和JMP指令位于同一函数中,但使用全局标号则不受限制。
  • JMP指令也可以创建一个循环,也就是在循环结束时用JMP指令再跳回循环开始的位置。
  • LOOP指令也可以创建一个循环代码块,ECX寄存器为循环的计数器,每经过一次循环,ECX的值减一。LOOP指令执行分为两步,第一步ECX值减一;第二步将ECX与0比较,如果ECX不为0,则跳转到标号地址处;如果ECX为0,则不发生跳转。

0x2.6 栈与函数调用

通常来说,编译器会默认分配足够程序自身使用的栈空间。在Linux上,可以使用‘ulimit -a’查看或更改当前系统默认的栈大小。

栈空间是计算机内存中一段确定的内存区域,也有着一些指针指向相应的内存地址,在x86架构中这个指针位于ESP寄存器,而在X86-64平台上为RSP寄存器。在计算机底层,栈主要的几个用途是:

  • 存储局部变量
  • 执行CALL指令调用函数时,保存函数地址以便于函数结束时正确返回
  • 传递函数参数

操作栈常用指令是PUSH和POP,即出栈和入栈。PUSH指令会对ESP/RSP/SP寄存器的值进行减法运算,并使其减去4(32位)或8(64位),将操作数写入上述寄存器中指针指向的内存中。POP指令是PUSH指令的逆操作,先从ESP/RSP/SP寄存器(即栈指针)指向的内存中读取数据写入其他内存地址或寄存器,再依据系统架构的不同将栈指针的数值增加4或8。

  • 栈是由高地址向低地址增长,所以入栈的时候栈指针是减去4或者8,出栈是增加4或者8。
使用栈保存函数返回地址

CALL指令调用某个函数时,下一条指令的地址作为返回地址被保存到栈中,等价于PUSH返回地址与JMP函数地址的指令序列。被调用函数结束时,程序将执行RET指令跳转到这个返回地址,将控制权交还给调用函数,等价于POP返回地址与JMP返回地址的指令序列。因此无论调用了多少层子函数,由于栈后入先出的特性,程序的最终控制权会回到main函数。

调用子函数这一行为使用了PROCENDP伪指令来定义,且需要分配一个有效的标识符,所有的X86汇编程序中都包含标识符为main的函数,这是程序的入口点,main函数不需要使用RET指令,但其他被调用函数结束时都需要通过RET指令交还调用函数。

CALL指令执行时,下一条指令的地址被压入栈中,被调用函数的地址则被加载到EIP寄存器。

使用栈传递函数参数

在x86平台程序中,最常见的参数调用约定是cdecl,其他的还有stacallfastcall,thiscall等。需要注意的是,我们可以用栈来传递参数,但不代表栈是唯一的传递参数的方式,在X86-64上,我们还可以通过寄存器传递参数。

假设func函数有三个参数arg1,arg2和arg3,那么cdecl约定下通常如下所示。

PUSH  arg3
PUSH  arg2
PUSH  arg1
CALL  func  ;PUSH   返回地址      JMP   函数地址
使用栈存储变量

由于MOV指令不允许将标志位寄存器的值复制到一个变量,因此使用PUSHFD指令就是保存标志位寄存器中标志位的最佳途径。PUSHFD指令是把32位EFLAGS寄存器的内容压入栈中,POPFD指令则是把栈顶的数据弹出至EFLAGS寄存器中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值