汇编

说明

对windows下的汇编、x86架构的学习
微软手册

PART 常识

指令周期

1.CPU从指令队列的内存取余取得指令,之后立即增加指令指针的值
2.CPU对指令的二进制位模式进行译码。这种位模式可能会表示该指令有操作数
3.如果有操作数,CPU就从寄存器和内存中取得操作数。有时,这步还包含了地址计算
4.使用3得到的操作数,CPU执行该指令。同时更新部分状态标志位(FLAGS)
5.如果输出操作数也是该指令的一部分,CPU还需存放其执行结果
通常简化为三个步骤取指、译码、执行

读内存步骤

1.将地址放置于地址总线
2.设置处理器读取引脚
3.等待一个时钟周期给存储器芯片进行响应
4.将数据从数据总线复制到目标操作数
以上每一步通常只需要一个时钟周期,访问寄存器只需要一个时钟周期

操作模式

从80386开始,CPU有三种工作方式:实模式,保护模式和虚拟8086模式。只有在刚刚启动的时候是real-mode,等到操作系统运行起来以后就切换到protected-mode。实模式只能访问地址在1M以下的内存称为常规内存,我们把地址在1M 以上的内存称为扩展内存。在保护模式下,全部32条地址线有效,可寻址高达4G字节的物理地址空间; 扩充的存储器分段管理机制和可选的存储器分页管理机制,不仅为存储器共享和保护提供了硬件支持,而且为实现虚拟存储器提供了硬件支持; 支持多任务,能够快速地进行任务切换(switch)和保护任务环境(context); 4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码和数据的安全和保密及任务的隔离; 支持虚拟8086方式,便于执行8086程序。
虚拟的8086模式:

系统管理模式

由硬件生产厂家实现,向操作系统提供了实现电源管理和系统安全等功能的机制。

实模式:

早期的Intel处理器的编程环境,但增加了一些其他特性。当程序需要直接访问硬件和系统内存时,这种模式很有用
内存寻址方式为:段式寻址,即物理地址=段地址*16 + 段内偏移地址
可寻址任意地址,所有指令都相当于工作在特权级。
dos工作在实模式下

保护模式:

处理器的原生状态。该状态下,所有的指令和特性都可用
内存寻址方式为:支持内存分页和虚拟内存
支持多任务,可依靠硬件用一条指令即可实现任务切换,不同任务可工作在 不同的优先级下,操作系统工作在最高优先级0上,应用程序则运行 在较低优先级 上。从实模式到保护模式,需要建立GDT、IDT等数据表,然后通过修改控制寄存 器CR0的控制位(位0)来实现。
Windows工作在保护模式下。

虚拟8086模式:

虚拟8086模式是运行在保护模式中的实模式,为了在32位保护模式下执行纯16位程序。它不是一个真正的CPU模式,还属于保护模式。
内存寻址方式:段式寻址,与实模式一样
支持多任务和内存分页
v86模式主要是为了在保护模式下兼容以前的实模式应用,即可支持多任务,
但每个任务都是实模式的工作方式。

寄存器

8个通用寄存器

EAX : 用于函数返回值,并且乘除指令默认使用EAX
EBX
ECX : 默认使用ECX作为循环计数器
EDX
ESP : 用于寻址堆栈数据
EBP : 在函数调用期间保持不变,用来寻址局部变量
ESI : 用于高速存储器传输指令,扩展源变址寄存器
EDI : 用于高速存储器传输指令,扩展目的变址寄存器

6个段寄存器

实地址模式中,16位段寄存器表示的是预先分配的,内存区域的基址
保护模式中,存放段描述符表指针

其他

EIP,EFLAGS(控制标志位,状态标志位)

MMX寄存器

8个64位的MMX寄存器支持称为SiMD的特殊指令

XMM寄存器

x86包含8个128位的XMM寄存器,用于SIMD流扩展指令集

浮点单元(FPU)

Intel486处理器开始,FPU已经集成到主芯片上

PART 汇编基础 (MASA)

cdecl、stdcall、fastcall

cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,
所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式


stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压
栈方式,自己在退出时清空堆栈


fastcall方式的函数采用寄存器传递参数

函数

call指令有相对、绝对、近程、远程跳转
设计cs,ip寄存器

proto伪指令---函数声明

函数名 proto [距离] [语言] [参数1]:数据类型,[参数2]:数据类型,……
func_name proto stdcall arg1:DWORD,arg2:DWORD

proc伪指令---函数定义

proc stdcall arg1:DWORD,arg2:DWORD
函数体
fun_name endp

invoke伪指令–函数调用

使用invoke伪指令会帮你完成参数校检和压参操作,也就是说不用写压参的push指令。直接和高级语言一样直接调用函数即可
invoke func_name,100,100

EQU、TEXTEQU伪指令

和宏差不多,字符串替换
PI EQU<"3.1415926",0>
name EQU expression
name EQU symbol

数据声明

浮点用REAL4、REAL8、REAL10等
arr DWORD 10,20,30,40
DWROD 11,11,13,13
str BYTE "abcd"
sum DWORD 100
BYTE 20 DUP(0) // 20个字节,值都为0
BYTE 20 DUP(?) // 20个字节, 非初始化
BYTE 4 DUP("STACK") // 4 * 5 = 20个字节,STACKSTACKSTACKSTACK
如何计算数组大小?
list BYTE 10,20,30,40
list_length = (\(\$\) - list) // $ 当前地址计数器

PART 常见的汇编指令

相关伪指令

OFFSET 返回变量与其所在的段起始地址之间的距离
PTR 重写操作数默认的大小类型
TYPE sizeof(type)
LENGHTOF 返回数组中的元素个数
SIZEOF 返回数组初始化时使用的字节数
ALIGN 内存对齐指令
LABLE 对某段内存起别名,本身不申请空间

记号

reg寄存器
imm立即数
mem内存操作数

数据移动指令

PTE与间接操作数一起使用,用来标记指针的类型
inc BYTE PTR [esi] 否则不指明指针类型,无法读取内存
mov type PTR [eax] 同上,指明指针类型
mov eax val
mov eax [arr + i * 4 + 5] eax = arr[i * 4 + 5]
lea eax,[esi+ebx*4] eax = esi + ebx * 4 // 也就是装载有效地址
mov eax, OFFSET arr 编译期计算arr的地址,然后替换为mov eax,0x4300这样

movzx 零扩展
movsx 符号位扩展

xchg swap
LAHF 将EFLAGS寄存器的低字节复制到AH
SAHF 保存AH内容到状态标志位

PART 算数运算指令

shl 左移
shr 右移
sal 算术左移
sar 算术右移
rol 循环左移
ror 循环右移
rcl 带进位的循环左移
rcr 带进位的循环右移
shld 双精度的左移
shrd 双精度的右移
inc eax eax++
dec eax eax--
add eax ebx eax += ebx
sub eax ebx eax -= ebx
neg eax 求补码
运算结果会影响EFLAGS
test eax [ebx] &=,只更新EFLAGS,不更新eax
cmp eax [ebx] -=,只更新EFLAGS,不更新eax

过程调用

主要需要处理的问题,平衡堆栈,传参数,处理返回值
特别是,在函数内多控制流的情况下,平衡堆栈

cdecl——call的调用规范

stdcall 调用规范

Microsoft 遵循固定模式实现 64 位编程中的参数传递和子程序调用,该模式被称为 Microsoft x64 调用规范(Microsoft x64 calling convention)。它既用于 C 和 C++ 编译器,也用于 Windows API库。

只有在要么调用 Windows 函数,要么调用 C 和 C++ 函数时,才需要使用这个调用规范。它的特点和要求如下所示:

  1. 由于地址长为 64 位,因此 CALL 指令把 RSP(堆栈指针)寄存器的值减去8。

  2. 第一批传递给子程序的四个参数依次存放于寄存器 RCX、RDX、R8 和 R9。因此,如果只传递一个参数,它就会被放入 RCX。如果还有第二个参数,它就会被放入 RDX,以此类推。其他参数按照从左到右的顺序入栈。

  3. 长度不足 64 位的参数不进行零扩展,因此,其高位的值是不确定的。

  4. 如果返回值的长度小于或等于 64 位,那么它必须放在 RAX 寄存器中。

  5. 主调者要负责在堆栈中分配至少 32 字节的影子空间,以便被调用的子程序可以选择将寄存器保存在这个区域中。
    影子空间
    其实就是主调程序 为被调用程序预留的 4个 64位存储空间 可通过直接寻址方式访问。。这是WINDOWSAPI 64 的规范。具体用不用如何使用是 WINDOWS API 的事情(外部调用过程 无法自己修改)一般来说是用来暂存 传递参数的寄存器的值得 腾出来寄存器 以便 外部库过程使用。且外部库过程使用堆栈结束后需清除所有参数使用的堆栈空间和影子空间 需把堆栈指针复原到RSP指针未因为外部库过程而被修改之前的地址。

  6. 调用子程序时,堆栈指针(RSP)必须对齐 16 字节边界。CALL 指令将 8 字节的返回地址压入堆栈,因此,主调程序除了把堆栈指针减去 32 以便存放寄存器参数之外,还要减去8。

  7. 被调用子程序执行结束后,主调程序需负责从运行时堆栈中移除所有的参数和影子空间。

  8. 大于 64 位的返回值存放于运行时堆栈,由 RCX 指出其位置。

  9. 寄存器 RAX、RCX、RDX、R8、R9、R10 和 R11 常常被子程序修改,因此,如果主调程序想要保存它们的值,就应在调用子程序之前将它们入栈,之后再从堆栈弹出。

  10. 寄存器 RBX、RBP、RDI、RSI、R12、R13、R14 和 R15 的值必须由子程序保存。


假设传递的参数的个数为n,那么

当n为奇数时,调用者需要分配0x20 + 8 + (n - 1) * 8的栈空间

其中,0x20=32,用来保存调用者本地变量RCX, RDX, R8, R9,8字节用来保持栈地址16字节对齐,(n-1)*8大小的空间用来保存参数,按照从右向左的顺序,虽然低0x20大小的空间不使用,这部分参数通过RCX, RDX, R8, R9传递,但仍需要调用者为开始的四个参数分配空间。

当n为偶数时,调用者需要分配0x20+(n-1)*8的栈空间

这里,少了8字节,因为参数的个数为偶数,此时无需额外的8字节便可以保持栈地址16字节对齐

汇编常识

栈操作

栈向下增长,esp栈指针

push eax
eps -= 4
*(esp) = rax

pop eax
eax = *(esp)
esp += 4

未定义行为:
pop esp
push esp

比较大小

cmp = sub,除了不产生赋值,只更新flages寄存器
test = and,同上

如何在比较大小中区分有符号和无符号指令
存在两套指令,但是前置的比较操作是相同的
cmp a b
setl

cmp a b
setb

posted @ 2021-10-13 20:51  XDU18清欢  阅读(165)  评论(0)    收藏  举报