windows下32位汇编语言学习笔记 第一章

第一章 背景知识

80x86处理器的存储器

4个数据寄存器
EAX,EBX,ECX,EDX 

EAX寄存器
所有API函数的返回值都保存在EAX里,注意是返回值,不是返回参数,本书3.2.2 节,说是winapi的返回值,
而经过我测试,自定义函数的返回值也一样保存在eax里。


EBX寄存器
这个寄存器被windows用来保存指针使用前后必须push pop,EDI,ESI,ESP也是一样,汇编定义函数有个uses 指令,如果要用,
就在uses后面写上寄存器的名字。比如 _mypro proc uses ebx,para1,para2 ,这样编译后就自动在前面加上push ebx,返回前加上 pop ebx。



ECX,EDX,这俩随便用

汇编的这几个寄存器,感觉就像是全局变量,随便用,挺方便,但要记住几个关键寄存器使用前要保存,使用后要恢复,而且最后把返回结果mov 给eax就行。


2个变址指针寄存器
EDI,ESI

这俩寄存器,目前我看到的章节里,没有一个代码用到,不知道一般是做什么用的。

2个指针寄存器
EBP,ESP

EBP 
存取堆栈的指针,栈内的参数就是用这个来取的。先push ebp 然后 mov ebp,esp 然后 ebp - x 来取参数,最后也要pop 出来,恢复原先的值。


ESP
栈顶的指针,这个栈是向下扩展的,栈的大小可以设定,默认是大小有的说是64k,有的说是1M,可以用 .STACK[字节数]来指定栈的大小。

每个线程都有自己的栈,栈就是用来保存参数,局部变量,和返回地址。堆栈的大小是有限制的,所以压多少就要弹出多少。

用于操作栈的指令:
push xxxx 压栈,pop xxxx 出栈,pushad 把八个通用寄存器依次压栈,popad 八个寄存器弹栈,也就是常说的保护现场环境。

堆栈平衡:每个线程(进程只是个内核对象,线程才是执行程序功能的)都有自己的栈,很明显栈的大小是有限制的,压了几个变量进去用完就要清除掉。
另外call子程序的时候,当执行call的时候,就把调用地址压入栈,子程序中你不清除压入栈的变量,ret就没法返回到调用处了。

以前的dos程序函数参数比较少,可以通过寄存器传递,到了win32,API的参数猛增,就4个数据寄存器怎么能够用,所以就用栈来传递参数。


8个通用寄存器全是32位的DWORD类型,这也是win32API参数全是DWORD类型的原因,操作起来方便。
感觉汇编反而方便了,数据类型不用太操心,不用定义变量保存函数返回值,还有4个全局变量(4个数据寄存器)随时可以用,感觉还挺舒服的。




80x86处理器的工作模式:

实模式,保护模式 虚拟86模式,实模式,虚拟86模式就是为了兼容老软件,系统应用而做的一种向下兼容的功能,没必要深入了解,保护模式才是目前win32 下CPU的工作模式。


保护模式下最关键的地方就是内存寻址空间增加到4G;采用了优先级机制,分四个级别0-3,0级系统级最高,3级用户级别最低,1 2 是为了兼容alpha设置的,用不到。

经常说OD是ring 3级的程序调试工具,这个3级就是指保护模式里的3级用户级。

保护模式下用户级的程序不能够访问到系统级资源,通过级别的设置,用户级的程序无法通过提升自己的级别来操作系统级的资源。 



windows的内存管理

内存管理这块,也可以参照windows核心编程 内存管理部分

首先每个进程自己的4G寻址空间不是完全可用的

NULL指针区域
0x00000000-0x0000FFFF:65535字节 这个区域的作用是用来帮助程序员发现内存分配失败后未检查就使用的错误。
比如使用malloc分配内存失败,返回NULL,而又未做检查直接使用,如例子:就会产生内存非法访问的错误,提示程序员
int *piNum = (int*)malloc(sizeof(int));
int *piNpm = 5; 

以前一直不理解NULL的意思,一直以为就是个0,现在来看,这个空指针是有他的道理的,是利用了windows的内存管理机制做的一个内存使用的检测手段。
现在看NULL定义为0-65535之间的任何数都可以达到,检测指针区域的效果。

64K禁入区域
0x7FFF0000-0x7FFFFFFF:64K字节 用来隔离用户空间和内核空间,是一个分界线。

实际上进程可用的地址空间最后是到0x7FFE1000,到0x7FFF0000之间的60K内存空间就不让使用了。可以用Chect Engine 的Memory regions 查看进程的内存空间情况。


windows内核空间
0x80000000-0xFFFFFFFF:2G 这个分区用来保存操作系统代码,内存管理,线程调度,文件系统支持,网络支持,和所有设备驱动代码都存放在这里,这个区域被所有进程共享。同样也是保护的,不可访问。

其中0x80000000-0xC0000000:1G 用来加载系统所需DLL,SYS,可以用Process Explorer 查看System进程可以看见系统自己加载的模块,大部分是.sys驱动,dll只有ntdll.dll
nv4_disp.dll等极少数的dll模块,确实是所有设备驱动的代码都再这里。

这块的内存不能访问,我想这也就是为啥驱动级的保护壳厉害,就厉害在这里...

剩下的1G 0xC0000000以后的内存,不知道怎么看,windows好像就没提供操作这块内存的API.

必须推荐下Process Explorer,这个应该是windows下功能最强的进程管理器了,看线程,进程,进程模块等等信息非常方便。还没用上的一定要试试。



用户空间
0x00001000-0x7FFFFFFF:2G-128K 可执行文件和用户自己的dll都加载到这个空间。系统DLL加载到系统内核空间

其中0x00001000-0x00400000 是Dos兼容分区,这个还有用么?4M的空间...

0x00400000-0x10000000 是进程相关内容存放区域,这就是为啥默认的可执行文件加载地址是从0x00400000开始

0x10000000-0x80000000 是用户DLL映射空间,这就是为啥默认的dll文件加载地址是从0x10000000开始

从上面看出,并不是所有4G的寻址空间都是可用的,实际可供进程使用的只用2G-128K的空间。
一直有一个说法,windowsXP无法管理2G以上的内存,实际上是:一个进程里无法使用2G以上的内存空间,即使你有4G的内存,一个进程,也只能使用其中的2G。
但是别忘了每个程序都可以用2G的内存,你物理内存越大,可以同时打开的进程就越多。话说回来,一个程序需要2G以上的内存运行,谁用?...
说windows无法管理2G以上的内存应该是断章取义的说法。



分配粒度和内存页面大小

x86处理器平台的分配粒度是64K,32位CPU的内存页面大小是4K,64位是8K,保留内存地址空间总是要和分配粒度对齐。一个分配粒度里包含16个内存页面。

这是个概念,具体不用自己操心,比如用VirtualAllocEx等函数,给lpAddress参数NULL系统就会自动找一个地方分配你要的内存空间。如果需要自己管理这个就累了......

一个分配粒度是64K,这就是为什么Null指针区域和64K进入区域都是 64K的原因,刚好就是一个分配粒度。
一个内存页是4K,这就是为什么PE文件中的section都是0x1000对齐.
硬盘扇区大小是512字节,这就是为什么PE文件默认文件对齐是0x200.

这些数字绝对不是心血来潮设定出来的,而是综合了硬件结构和操作系统架构设定的。


内存页面的各种属性

PAGE_NOACCESS 禁止写入执行读取
查看进程内存区域能发现,NOACCESS属性的内存页面都是FREE状态的(未提交使用的内存区域),只有内存区域最后的0x7FFE1000-0x7FFF0000之间的60K内存区域状态是Reserve。(保留了,不让使用...)

PAGE_READONLY PAGE_READWRITE PAGE_EXECUTE 根据字面就很好理解

PAGE_WRITECOPY PAGE_EXCUTE_WRITECOPY 这2个页面属性是windows节省内存应用的一个机制.

难道要2个一样的可执行程序同时运行时各占一个独立4G的寻址空间么?既然是一样的程序,2个程序的代码段,数据段都是相同的。为了节省内存,windows就让2个进程共享单个内存块。
但是如果一个程序中的内存发生变化,另一个也同时发生变化,那岂不乱套了?开2个IE浏览网站,但是2个都显示同样的内容那还有什么意义?copy-on-write就是为解决这个问题而设置的。

PAGE_WRITECOPY 数据段
简单的说,2个一样的程序运行,如果内存中数据不发生变化,那么这段数据是共享的,如果其中一个程序的内存发生变化,比如记事本A写了一行字,那么就会把记事本的这个数据段复制出来
一份放到新的内存区域让记事本A单独使用,这时候记事本A和记事本B进程的数据段就不再共享,而是各自用各自的。但是他们的代码段还是共享。

PAGE_EXCUTE_WRITECOPY 代码段
代码段也是一样,你用OD修改了A记事本中的代码段,系统就会自动把A记事本的代码段复制一份新的,不再和B共享,也就不会影响B记事本中的代码段。
实际上一个程序的代码段,资源段等数据也没多大。所以,这种机制也看不太出来能节省很大的内存。



关于内存单位
内存单位再书里,汇编里,都是用16进制单位描述的,10进制看习惯了,突然全16进制我就比较不习惯。我把常用的列出来,看长了就能有个大概的概念了,突然来个0x165700,
也能不用计算器就能估算个大概。

0x100 256bit,0x200 512bit,0x400 1K,0x800 2K
0x1000 就是4K,0x10000就是64K,0x100000 1024K


用户空间里的0x00001000-0x00400000 的Dos兼容分区,现在还有用么,按照书上的说明,进程堆,内存非配堆,都再0x00400000-0x10000000区域里,那么如果我们设置可执行文件的加载地址从
0x00001000开始,是否进程空间就又能多出4M的内存区域可供使用呢?



一开始想速度看完第3章了解语法就能开始干些"实事",结果直接跳到17章去看PE和PE程序才发现还是太多看不懂,意识到这本书不能跳跃式阅读了,必须从头开始打一个好基础。
而且我也发现对windows32 API也相当的不熟悉,所以看这本书的时候是和"Windows程序设计","Windows核心编程"穿插阅读,后两本以前都看过,但是比较肤浅。这次学习汇编
在穿插看看加深下理解。

写这个学习笔记一是想培养自己养成写文档的习惯,把阅读后的重点都写下来,既能加深记忆又能培养写作能力,再者,也期望能有高水平的朋友能把我理解不正确的地方或者重点
的地方给个更好的指点,当然如果笔记能给和我一样的初学者一个好的学习参考,那就更好了


星期四, 四月 30, 2009