跳至主要內容

汇编

chanchaw大约 9 分钟game-plugin

概述

注释

英文分号作为注释,类似 java 中的双斜线

__asm {
    push ebp
    mov ebp,esp ;标准的函数调用头,遇到这两行代码表示开始调用函数
    ...
}

指令介绍

REP

REP指令,重复前缀指令,不可独立使用。
用途:重复执行一条指令
形式:位于 stos、lod、ins、outs等传送指令之前,如 rep stosd edi
运行机制:rep指令是重复执行该指令后面的汇编代码,执行次数由寄存器ecx控制

操作内存

DWORD R4(UINT_PTR addr){
    __try {
        return *(DWORD*)addr;// 相当于ReadProcessMomory
    }__except(1){
        return 0;
    }
}

参数与返回值

参数个数

看到汇编代码中有 [ebp+8] 一般表示 call 函数的第一个参数,类似的 [ebp+C] 表示第二个参数,类推 [ebp+10] 表示第三个参数

call返回值

一般 call 调用函数的返回值会保存到 eax 中,那么如果要将汇编指令执行后的到的返回值返回给 cpp 源码使用则如下操作:

const char* roleName="player";
UINT_PTR retVal = 0;// 用于接收返回值
UINT_PTR funcAddr = 0x60C1F0;// 调用的call的起始地址(函数的起始地址)
__asm {
    lea eax,roleName
    push eax
    call funcAddr
    add esp,4 // 外平栈
    mov retVal,eax // 返回值赋值给cpp变量
}

寄存器

介绍通用寄存器

EAX: 累加器,加减和比较运算都借助 EAX 来达到指令优化的效果,乘除必须在 EAX 中进行。
EDX:数据寄存器,EAX 的延伸。
ECX:计数器
ESI:源变址寄存器,存储输入数据流位置信息,“读”
EDI:目的变址寄存器,指向相关数据操作结果存放位置,“写”
ESP:栈指针,始终指向函数栈的最顶端
EBP:基址指针,被用于指向函数栈的最底端
EBX:通用寄存器
EIP:始终指向当前正在执行的指令

段寄存器ES CS SS DS FS GS

概述

段寄存器的产生源于Intel 8086

CPU体系结构中数据总线与地址总线的宽度不一致。数据总线的宽度,也即是ALU(算数逻辑单元)的宽度,平常说一个CPU是“16位”或者“32位”指的就是这个。8086CPU的数据总线是16位。

地址总线的宽度不一定要与ALU的宽度相同。因为ALU的宽度是固定的,它受限于当时的工艺水平,当时只能制造出16位的ALU;但地址总线不一样,它可以设计得更宽。地址总线的宽度如果与ALU相同当然是不错的办法,这样CPU的结构比较均衡,寻址可以在单个指令周期内完成,效率最高;而且从软件的解决来看,一个变量地址的长度可以用整型或者长整型来表示会比较方便。但是,地址总线的宽度还要受制于需求,因为地址总线的宽度决定了系统可寻址的范围,即可以支持多少内存。如果地址总线太窄的话,可寻址范围会很小。如果地址总线设计为16位的话,可寻址空间是2^16=64KB,这在当时被认为是不够的;Intel最终决定要让8086的地址空间为1M,也就是20位地址总线。地址总线宽度大于数据总线会带来一些麻烦,ALU无法在单个指令周期里完成对地址数据的运算。有一些容易想到的可行的办法,比如定义一个新的寄存器专门用于存放地址的高4位,但这样增加了计算的复杂性,程序员要增加成倍的汇编代码来操作地址数据而且无法保持兼容性。

Intel想到了一个折中的办法:把内存分段,并设计了4个段寄存器,CS,DS,ES和SS,分别用于指令、数据、其它和堆栈。把内存分为很多段,每一段有一个段基址,当然段基址也是一个20位的内存地址。不过段寄存器仍然是16位的,它的内容代表了段基址的高16位,这个16位的地址后面再加上4个0就构成20位的段基址。而原来的16位地址只是段内的偏移量。这样,一个完整的物理内存地址就由两部分组成,高16位的段基址和低16位的段内偏移量,当然它们有12位是重叠的,它们两部分相加在一起,才构成完整的物理地址.

CS:代码段寄存器,存放当前正在运行的程序代码所在段的段基址,标识当前使用的指令代码可从该段寄存器指定的存储器段中取得,相应的偏移量由IP提供。 Ds:当前程序使用的数据所存放段的最低地址,即存放数据段的段基址; Ss:堆栈段寄存器,存放堆栈的底部地址, Es:当前程序使用附加数据段的段基址,该段是串操作指令中目的串所在的段; Fs:标志段寄存器,80386起增加的两个辅助段寄存器之一,在这之前只有es; FS指向当前活动线程的TEB结构(线程结构); 偏移 说明 000 指向SEH链指针 004 线程堆栈顶部 008 线程堆栈底部 00C SubSystemTib 010 FiberData 014 AribitraryUserPointer 018 FS段寄存器在内存中的镜像地址 020 进程PID 024 线程ID 02c 指向线程局部存储指针 030 PEB结构地址(进程结构) 034 上个错误号 Gs:全局段寄存器

从寄存器拷贝数据到内存

从段寄存器 fs 拷贝数据到内存中需要使用寄存器作为中转

UINT_PTR memAddr = 0x111;
__asm {
    mov eax,FS:[0x18]
    mov memAddr,eax
}

获取当前线程的TEB首地址

段寄存器 fs 的偏移 0x18 地址保存的是段寄存器的首地址

UINT_PTR getThreadTEB(){
    UINT_PTR memAddr = 0;
    __asm {
        mov eax,FS:[0x18]
        mov memAddr,eax
    }
    return memAddr;
}

esp,ebp

ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶(下一个压入栈的活动记录的顶部),是栈指针。
EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面(这里没错,就是最上面的)一个栈帧的底部(当前活动记录的底部),是帧指针。

esp是栈顶指针寄存器,ebp是栈底指针寄存器。

函数

send

汇编send
汇编send

call

汇编call
cpp 代码中嵌入的汇编中使用 call 不可硬编码函数地址,只能 call 一个变量

UINT_PTR funcAddr = 0x123456
__asm {
    call funcAddr
}

函数运行在汇编层面的总结

1、函数的运行都是在栈上开辟内存。 2、栈是通过 esp(栈顶指针)、ebp(栈底指针)两个指针来标识的。 3、对于栈上的访问都是通过 ebp(栈底指针)的偏移来访问的。 4、在调用一个函数时,有两件事情要做:先将调用函数的下一行指令的地址压入栈中;再进行跳转。 5、在函数调用时检查函数是否申明、函数名是否相同、函数的参数列表是否匹配、函数的返回值多大。 5.1 如果 【函数的返回值<=4 个字节】,则返回值通过寄存器 eax 带回。 5.2 如果 【4<函数的返回值<=8 个字节】,则返回值通过两个寄存器 eax 和 edx 带回。 5.3 如果 【函数的返回值>8 个字节】,则返回值通过产生的临时量带回。 6、函数结束 ret 指令干了两件事:先出栈;再将出栈的值放到 CPU 的 PC 寄存器中。因为 PC 寄存器中永远放的是下一次执行指令的地址,所以就顺理成章的在函数调用完之后依旧接着原来的代码继续执行。

断点

断点类型

软件断点:由非法指令异常实现,适用于运行于内存中的程序(软件实现)。以x86为例,向某个地址打入断点,实际上就是往该地址写入断点指令INT 3,即0xCC。目标程序运行到这条指令之后就会触发SIGTRAP信号,gdb捕获到这个信号,根据目标程序当前停止位置查询gdb维护的断点链表,若发现在该地址确实存在断点,则可判定为断点命中[1]。 硬件断点:由硬件特性实现(数量有限),适用于直接在flash中运行的程序。

软件断点和硬件断点的区别

既然软件断点是要往某个地址写入断点指令的,那么最起码该地址应该是可写的吧?大多数时候,我们的程序是会被加载到内存(RAM)中执行的,RAM是可读可写,这时候软件断电就是有效的[2]。 但是,对于某些比较重要的程序,可能会直接在flash中执行,并且flash对用户可能是只读的,这时候软件断点就没有用了,因为没办法写进断点指令,此时必须依赖于硬件断点。这就是软件断点和硬件断点使用上的不同。

gdb中设置断点

软件断点:break 命令 硬件断点:hbreak命令

案例

cpp变量入栈

// 第一种方法,直接将 cpp 变量压入栈
const char* roleName="player";
__asm {
    push roleName
}

// 下面方法有问题,变量 roleName 本身保存的是字符串的地址,那么使用 `lea eax,roleName` 的话
// 是取字符串的地址的地址压入栈了,所以此处应该采用 push roleName 将字符串的地址压入栈
// 如果想要使用 lea eax,变量1    则变量1不可是指针,应该是普通变量
const char* roleName="player";
__asm {
    lea eax,roleName
    push eax
}

赋值操作案例

汇编中的立即数可以使用前缀 0x 或者后缀 h汇编基础案例

数组的使用

float coordinate[3] = {11,22,33};// 三维坐标系
// 下面汇编的第一行直接 push 数组不对
// 应该取数组首地址后 push(2-3行)
__asm {
    push coordinate
    lea eax coordinate
    push eax
}