汇编
概述
注释
英文分号作为注释,类似 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

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
}
