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
这俩寄存器,在字符串操作方面真是恰倒好处,主要配合rep\repz\repnz\repe\repne\cmpsb\movsb\stosb\lodsb\cmpsw等。比如将一个szMsg的字符串复制到内存指针lpMem,并且去掉字符串中的大写A:
pusha
lea esi,szMsg
mov edi,lpMem
@@:
cld
lodsb
cmp al,0
je @F
test al,'A'
jz @B
stosb
jmp @B
@@:
popa
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核心编程"穿插阅读,后两本以前都看过,但是比较肤浅。这次学习汇编
在穿插看看加深下理解。
写这个学习笔记一是想培养自己养成写文档的习惯,把阅读后的重点都写下来,既能加深记忆又能培养写作能力,再者,也期望能有高水平的朋友能把我理解不正确的地方或者重点
的地方给个更好的指点,当然如果笔记能给和我一样的初学者一个好的学习参考,那就更好了
windows下32位汇编语言学习笔记 第二章 准备编程环境
Win32可执行文件的生成过程:
Win32下的可执行文件就是常说的PE文件。对于汇编来说,生成的步骤如下:
1.编辑.asm格式的汇编源代码.
2.用ml.exe 将.asm源码生成.obj文件.
3.用link.exe 将.obj .rc(资源文件,用于windows窗口界面定义的一种格式)连接成.exe文件.
C API编程生成步骤和上面几乎一样。
1.编辑.C格式的源代码。
2.用cl.exe(vs2008编译器)或者gcc.exe(GCC编译器),生成.ojb .o文件.
3.用link.exe 连接成.exe 文件。
其中的.rc是可选的,如果只是简单的创建一个窗口,使用默认的界面样式,不需要编辑RC文件。
因为我看这本书是结合"windows程序设计"和"windows核心编程"一起阅读,所以在相关章节,我会结合后两本书并把要点写明。
开发环境的建立:
若要做其事,先要利其器,选好了工具学习使用起来就会更方便。
asm文件编辑器的选择
我使用的是Scite,一个开源的免费的,支持windows,linux的编辑器,支持N种种语言的语法高亮。
可自定义菜单功能。可设置不同语言的F1帮助,支持.chm .hlp .col(msdn帮助格式) F1键打开。
占用内存少,打开文件速度快,优点数不胜数。以前一直是用NotePad++的,后来发现其作者居然在主页上公开抵制奥运发表政治看法,就再不使用。
简单说下配置方法,详细的百度,google下自己搜索。
1.配置编译
Scite 里默认的编译快捷键是F7,打开asm.properties配置文件照下面修改
我这里下载了个MASMPlus,是国人开发的一个汇编IDE,里面就包含了所需要的连接编译命令和windows .inc .lib 库,其实这个编辑器也不错。
#MASM路径设置,
asmpath=D:/Program Files/MASMPlus
下面这个语句就是绑定快捷键盘F7的编译命令
/I 参数是必须的,指定include 的路径。其他的具体看书上的说明
command.compile.$(file.patterns.asm)=$(asmpath)/bin/ml.exe /I "$(asmpath)/include" /I "$(FileDir)" /c /coff /nologo /Fo $(FileName).obj $(FileNameExt)
#ctrl+ F7 连接命令
/LIBPATH 这个必须指定到Lib目录
make.command=link
command.build.*.asm=$(asmpath)/bin/link.exe /LIBPATH:"$(asmpath)/Lib" /LIBPATH:"$(asmpath)/Exlib" /LIBPATH:"$(FileDir)" /SUBSYSTEM:WINDOWS /nologo /OUT:$(FileName).exe $(FileName).obj *.res
里面用到了很多Scite自带的变量必须 $(FileDir)文件绝对路径 $(FileName)文件名 $(FileNameExt)文件名带扩展名,等等,也可以自定义,比如上面的asmpath就是我自定的路径。
配置好以后,就可F7编译,ctrl+F7连接了。绝对不建议使用radasm这样的复杂IDE环境,功能是很强,但是大部分的功能初学者根本用不到。不要迷失在IDE环境中....
关于link
link有个参数 :BASE ,意思是把程序装入指定的地址。上一章的时候说过0x00001000-0x00400000 是DOS兼容的内存区域,不写dos程序这块地址浪费了可惜,看看能不能把装入地址设置到这里。
试下能不能装到BASE:0x00010000地址,因为分配粒度是64K装入地址必须是分配粒度的倍数,这里刚好就是64K,也是整个内存空间的第一个分配粒度地址,这里编译的就是第二章的Test.asm
使用MASMPlus自带的 link 5.12.8078 连接后提示:错误的windows 95地址...
LINK : warning LNK4096: /BASE value "10000" is invalid for Windows 95; image may not run
使用vs2008自带的 link 9.00.30729.01
这次成功了,运行test.exe后查看装入地址,用Process Explorer导出看看,果然载入基址就是0x10000这个地方,后面的0x4000是大小.
Test.exe D:/My document/book/汇编/罗云彬WINDOWS环境下32位汇编语言程序设计第2版(清晰+源码)/附书光盘里面的内容打包/Chapter02/Test/Test.exe 0x10000 0x4000
看来微软也意识到,现在不会再有人开发dos兼容程序了,0x400000以前这块内存地址也不用留给16位程序了,编译器也不再提示。
但如果不指定,默认还是装入到0x00400000这个地址,也许大家都习惯了,微软也不想随便就把首选装入地址改掉。
但是有个问题,是不是首选地址往前移了0x3F0000就说明应用程序又多了3M多的内存寻址空间可以使用呢?
在试下往后最大能加载到什么地址
我们知道,0x7FFF0000后面就是64K禁区,这里就是用户空间和内核空间的分界线。
先试下0x7FFE0000 ,可以连接,但是运行就提示
---------------------------
Test.exe - 应用程序错误
---------------------------
应用程序正常初始化(0xc0000018)失败。请单击“确定”,终止应用程序。
---------------------------
确定
---------------------------
这个初始化代码我搜索vs2008的所有头文件,在ntstatus.h里查到下面的描述:
// {Conflicting Address Range}
// The specified address range conflicts with the address space. 指定的地址范围与地址空间冲突?(我英文很差)
//
#define STATUS_CONFLICTING_ADDRESSES ((NTSTATUS)0xC0000018L)
实际上到了这个地址,就已经不让使用了。虽然可以连接,但是运行就出错,用了不该用的地址。
上一章说过,虽然64K禁区开始地址是0x7FFF0000但实际上从0x7FFE1000开始最后这段内存空间已经被标记成Reserve状态的PAGENOACCESS,所以从0x7FFE开始后面的128K 内存寻址空间就已经不能用了
64K禁区增加到了128K,呵呵
最大的加载地址只能是0x7FFD0000,这是最后一个可利用的分配粒度边界。
综上所述,win32 程序可以使用的内存寻址空间是:0x00010000-0x7FFD0000 这段区域,也就2G多一点。 传说中4G的寻址空间实际能自己支配的只用一半,呵呵,郁闷不?
特别注意下,link.exe可以使用vs2008自带的最新的,ml.exe可不行,编译就出错,提示windows.inc里的一段数据初始化失败,可能是配套的inc不是新版的缘故。
写到这里我也发现MASMPlus自带的inc和lib都比较老,从这里开始就直接使用RadASM里的inc和lib.ml.exe 和link.exe 就直接使用vs2008里自带的。
/STACK参数 格式:/STACK:reserve[,commit]
第一个reserve参数的数值是指保留的栈大小&