0x01 前言

DOS下16位汇编学的时候完全就是硬啃,因为和现在用到的知识几乎不沾边,Windows下32位汇编开始涉及我们用到的知识,学起来还有点动力,本篇博客简单记录下Windows下32位汇编的学习总结

0x02 基本概念

CPU工作模式

从DOS下16位汇编到Windows下32位汇编,CPU的工作模式也从实模式变为保护模式(其实还有实模式和虚拟86模式,但本篇博客意在总结只挑重点说),这时CPU里的寄存器已经从16位变为32位,寻址空间也从之前的1MB变为4GB,并且多了两个东西,一个是内存分页机制,一个是优先级,内存分页机制可以为虚拟内存提供良好支持,后面内存管理中会提到,优先级包含4个级别,0-3级,0级就是常说的内核层ring0级,3级就是应用程序层ring3级,由于历史原因,Windows操作系统不使用1级和2级

内存管理

保护模式下Windows的内存管理和实模式下DOS的内存管理有很大不同,DOS下内存管理通过两个16位的寄存器得到一个20位地址,也就是最终的物理地址,而在保护模式下,首先,寄存器变为32位,2的32次幂=4GB,也就是说单个寄存器即可寻址4GB,不再需要之前(段地址:偏移地址)的形式,其次,由于内存分页机制,内存被分为一个个4KB大小的页,这些物理上不相邻的4KB大小的页通过一个叫“页表”的映射器,映射为虚拟地址,供操作系统和应用程序使用,人们常说的应用程序可以使用4GB,其实是因为CPU是一个分时处理器,例如,在0.001-0.002这个时间段,全部的4GB内存属于程序A,在0.002-0.003这个时间段,全部的4GB内存属于程序B,在0.003-0.004这个时间段,全部的4GB内存属主又切换回程序A,大概就是这个意思,从物理上看,实际的一块内存地址,可能上一秒属于程序A,下一秒属于程序B,从逻辑上看,一块内存地址,一直属于程序A

中断和异常

中断指当程序执行过程中有更重要的事情需要实时处理,通过中断控制器通知CPU,CPU保存好当前程序的地址后,转到中断处理程序,处理完后再通过保存的地址回来,异常指遇到指令异常、除法除0、权限异常等问题,转到异常处理程序,处理过程和中断处理程序一样,DOS下的中断和异常可以由用户控制,但是Windows下的中断和异常则有严格的限制,因为异常/中断处理程序的优先级比较高(优先级不高也没法优先进入中断),通过一个叫“门”的概念控制如何调用中断/异常处理程序

0x03 代码演示

Win32下汇编相比C语言不同的是,为语句找到他们该去的地方,比如代码段,数据段,堆栈段等,下面看一个反汇编例子
C语言代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

// 简单的函数,接受两个参数并返回它们的和
int add(int a, int b) {
int result; // 局部变量
result = a + b; // 执行加法运算
return result; // 返回结果
}

int main() {
int x = 5; // 第一个局部变量
int y = 3; // 第二个局部变量
int sum; // 用于存储结果的局部变量

sum = add(x, y); // 调用add函数

printf("Sum: %d\n", sum); // 打印结果
return 0; // 程序结束
}

上述代码的反汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
; main函数部分
main:
push ebp ; 保存旧的基址指针到堆栈
mov ebp, esp ; 设置新的基址指针,指向当前栈顶
sub esp, 16 ; 在栈上分配16字节空间给局部变量

; 初始化局部变量x = 5
mov DWORD PTR [ebp-4], 5

; 初始化局部变量y = 3
mov DWORD PTR [ebp-8], 3

; 准备调用add函数,先将参数压栈
mov eax, DWORD PTR [ebp-8] ; 获取y的值
push eax ; 将y压栈(第二个参数)
mov eax, DWORD PTR [ebp-4] ; 获取x的值
push eax ; 将x压栈(第一个参数)

call add ; 调用add函数,会将返回地址压栈
add esp, 8 ; 清理栈上的函数参数

; 将add函数的返回值保存到sum变量
mov DWORD PTR [ebp-12], eax

; 准备调用printf函数
mov eax, DWORD PTR [ebp-12]
push eax ; 将sum作为参数压栈
push OFFSET FLAT:.LC0 ; 将格式化字符串地址压栈
call printf ; 调用printf
add esp, 8 ; 清理栈上的参数

; 函数返回
mov eax, 0 ; 返回值0
leave ; 相当于mov esp, ebp; pop ebp
ret ; 返回

; add函数部分
add:
push ebp ; 保存main函数的基址指针
mov ebp, esp ; 设置当前函数的基址指针
sub esp, 16 ; 为局部变量分配空间

; 计算a + b,并保存到result
mov eax, DWORD PTR [ebp+8] ; 获取第一个参数a
add eax, DWORD PTR [ebp+12] ; 加上第二个参数b
mov DWORD PTR [ebp-4], eax ; 保存结果到result

; 返回结果
mov eax, DWORD PTR [ebp-4] ; 将result放入eax寄存器作为返回值
leave ; 清理堆栈
ret ; 返回,弹出返回地址

反汇编代码解读:
1、首先是常规的EBP入栈,用于保存上一个栈基址指针
2、然后将栈顶指针(ESP)的值赋给栈基址指针(EBP)
3、栈向下延伸16字节
4、接下来该调用函数add了,此时EBP-4是第一个局部变量x的地址,EBP-8是第二个局部变量y的地址,由于C/C++遵循的调用标准是cdecl,也就是参数入栈时是从右向左,所以先将局部变量y入栈,再将局部变量x入栈
5、程序准备进入函数add的栈帧,进入之前会先把返回地址压入栈
6、进入函数add的栈帧后,同样是EBP、ESP那一套
7、此时以当前栈帧的EBP为基址,变量x和变量y的地址就应该是EBP+8和EBP+12,计算完后弹出返回地址,继续执行函数add后面的指令