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核心编程"穿插阅读,后两本以前都看过,但是比较肤浅。这次学习汇编
在穿插看看加深下理解。
写这个学习笔记一是想培养自己养成写文档的习惯,把阅读后的重点都写下来,既能加深记忆又能培养写作能力,再者,也期望能有高水平的朋友能把我理解不正确的地方或者重点
的地方给个更好的指点,当然如果笔记能给和我一样的初学者一个好的学习参考,那就更好了
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参数的数值是指保留的栈大小,第二个commit参数值指定提交的大小。
比如,用/stack,0x100000 指定保留1MB的栈大小,这是在内存空间里保留出的堆栈大小,并不影响物理内存大小,因为还没提交。
/stack,0x100000,0x100000 则既保留的同时也提交这块内存,现在的物理内存已经被映射到这个内存空间,可以看见进程里test.exe的内存大小已经从700多K长到1.7MB了
提交和保留的
保留只是在内存空间地址上分配出指定的大小,准备使用,但还不能使用。可以通过 Process Explorer 查看test.exe进程的虚拟内存增大。
提交是把保留出来的内存空间地址和物理内存做映射,提交以后才能正真使用这块地址。这是后物理内存占用也增大了。
第一章曾今不确定默认栈大小,现在可以确定下来了,就是1MB,msdn里关于link的/stack帮助里明确写到"The default stack size is 1 MB"
那么栈保留在用户空间的什么位置?
msdn里没有说明,也可能我没查到。我设置了不同的stack大小,用Cheat Engine 查看内存空间情况(打开进程,点memory view 然后按 Ctrl + R)发现保留的地址总是在
0x00030000 这个内存空间地址上开始。这里有2个问题
1.为啥从这里开始 注:后面是纯粹我自己的理解(没找到相关参考),不对的地方请纠正。
内存空间是一块连续的区域,栈是由系统维护的,一旦指定了大小程序中无法更改。系统的原则是尽量往内存的高地址处放,如果放到中间的位置,有可能会导致出现内存空间出现碎片,
导致下一次提交保留内存比较大时还需要系统移动这些在内存空间中间的保留段,为给新分配的空间腾地方,这个过曾会造成频繁的和页面文件交换数据导致系统变慢。
2.0x00001000-0x00400000 DOS兼容内存区 真浪费了么
明显没有,从栈的空间地址的分配上就看出,0x00030000刚开就在这个区域里,这4M的空间其他的地方怎么用的暂且不说,起码默认1MB栈空间就用是这里的内存空间。
如果把载入首先地址地设置成0x00010000 情况如何呢?
这时候栈的内存区域起始地址就往下增加了在0x00040000 这个地址上。
所以担心系统会浪费进程仅可用的2G虚拟内存根本是没有必要的,反而系统会充分帮助你的应用程序使用这块空间。
RC编辑器
做复杂的界面就需要个RC资源编辑器了,放按钮,设置图片等等,如果纯写代码就比较麻烦了,可以下载个ResEdit-1.4.4.19 资源编辑器,本书例子里的RC资源可以直接用这个打开编辑。
可以再配置脚本里设置一个编译RC的快捷键
makepath=E:/compile/VC2008
command.name.1.*.rc=RC
command.1.*.rc=$(makepath)/bin/rc.exe /i "$(asmpath)/include" $(FileName).rc
这样用scite 打开按Ctrl +1 就可以编译成.ras资源文件了。
关于MAKE
在你的项目很复杂的情况下,每次ml后再 link就比较繁琐,就可以通过编辑makefile 使用make帮助自动编译。现在学习阶段代码都很小,没有复杂的文件关系,所以用不着这个东西。
主要是使用make必须写makefile文件,不能用参数解决,我现在还不知道如何用scite的配置自动生成模版文件,还是暂时不用了。
帮助文档
既然是windows32编程,用到的也都是win32API所以,msdn是必不可少的,这里推荐大家去http://www.skygz.com/ 下载msdn1.5绿色版,里面还有vs2008绿色版。
要养成看帮助的习惯,而不要把代码自动完成当做参考.
本章看起来比较轻松,重要的是自己手动搭配一个.asm 汇编的编程环境,还是那句话,不要用复杂的东西,高级的功能一个也用不到。大家可以用我推荐的Scite自己配置,
也可以使用MASMPlus。
windows下32位汇编语言学习笔记 第三章 使用MASM
本章讲述的是masm 汇编的程序结构,基本语法,定义等,本章这些内容只是汇编指令里比较常用的,在下面的章节将要用到的指令。实际上汇编指令远不止这些。感兴趣可以参照其他的汇编书籍了解一下。不过对于本书下面的章节来说,这些指令基本上够用了。
Win32汇编程序的基本结构
从例子可以看出来,Win32汇编的结构很简单,下面简单分析下。
模式定义
.386
.model falt,stdcall
option casemap:none
这个地方书上已经将的很清楚了。关于.386 .486 .586 .686 之类的指令集,我没找到资料,试验了一下写成.686也没什么问题。
include includelib语句
include windows.inc
includelib kernel32.lib
这里的include 和C语言里的include 头文件一个道理,都是导入预先声明好的函数,包括定义好的各种结构。
includelib 就是指定连接的时候告诉连接器从那个lib里找你通过include引入并使用的函数,win32API都是以动态链接库的形式提 供的,所以这里就需要对你使用的winAPI包含在那个dll里做到心中有数,不知道的就查msdn,每个API说明后面都有这个API包含在那个头文件 中,比如:
Header: Declared in Winuser.h; include Windows.h.
winAPI是C语言写的,所以头文件都是.h的,汇编的头文件声明是.inc的,打开kernel32.inc 找找Exitprocess 的申明 ExitProcess PROTO :DWORD
你也可以不用预定义的.inc头文件,自己定义。
如果你使用了函数确没有包含对应的.lib,比如使用了ExitProcess函数,没有includelib kernel32.lib,连接时就会报错:
error LNK2001: 无法解析的外部符号 __imp__ExitProcess@4
这个外部符号名就是你要调用的函数,名字很诡异吧,这里先有个了解,讲到调用约定的时候再详细说明。
段定义,程序结束和入口.
.data ;全局变量段
szTest db '消息窗内容',0
szCaption db '消息窗标题',0
.code 代码段
start:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
end start
这里的段就是PE格式里的Section (区块),一个PE文件(可执行)最少包含2个区块,代码块和数据块,区块的名只是方面记忆,对于系统来说是无 关紧要的。区块是按内存页对齐的(0x1000 4K)。区块的类型很多,比如.idata包含导入表,.rsrc,包含资源文件等等。但是,任何时候不 要通过区块名字来定位区块,从PE结构的IMAGE_SECTION_HEADER来定位区块才是正确的做法,因为区块名字是可以任意的。关于PE结构的 说明,我见过的最详细的就是“加密与解密第三版”第10章的介绍,大家可以去看看。
编译本章的hello.asm 用OD打开exe可以看见有3个区段,.text .rdata 和 .data ,.text 就是代码里的.code 段,.data就是代码里的.data段,.rdata没人定义怎么自己冒出来了,其实这就是hello.exe的导入表,因为程序里用到了2个外部 dll函数,MessageBox,ExitProcess。这个段就是编译器自动生成的。至于为什么叫.rdata,刚才说了,名字不是重要的,只是帮 助记忆,导入表区段有的名字可能就是.idata。
另外还需要注意,程序的入口必须自己指定,汇编里没有Main这样的程序执行起点,这点别忘了。
变量名,变量,数据结构
这个地方没啥好说的,多看,多写,慢慢就习惯了,值得注意的地方就是,变量的命名方式一定要按照后面代码风格所说,按照匈牙利表示法来命名,从一开始就养成一个好习惯。
子程序,函数的定义和使用
调用约定和名称修饰符
除了书上将的_cdecl,_stdcall等,还有一种c++builder里常用的_fastcall调用,__fastcall调用也是被调用的函 数负责清栈,参数的传递规则是,从左边开始不大于4字节的参数分别用edx,ecx传递,其他参数遵循从又右到左的顺序通过堆栈传递。
c c++在内部是通过函数修饰符来识别函数的,由编译器在编译时生成函数名称修饰符,而且,不同的调用约定不同的语言生成的修饰符定义名称不同,所以有必要了解一下函数的名称修饰符。
例子函数:int max(int,int);
对于C语言
_cdecl调用
名称修饰符是在函数前加一个下划线:_max
_fastcall调用
名称修饰符在函数前加一个@后面加一个@紧跟参数字节数:@max@8
_stdcall调用
名称修饰符在函数前加一个_后面加一个@紧跟参数字节数:_max@8
对于C++语言,不管任何调用约定,描述符都以?开头后边更函数名,然后是根据参数表查出的返回值类型,然后是参数类型,最有以@Z结束
?+函数名+调用规则名+返回类型+参数类型(从左到右)+@Z
其中调用规则名表:_cdecl:@@YA,_stdcall:@@YG,_fastcall:YI
标示符:参数类型
X:void,D:char,E:unsigned char,F:short,H:int,I:unsigned int,J:long,K:unsigned long,M:float,N:double,_N:bool,U:Struct
指针:PA,const指针:PB
对于max函数修饰名称就是:?max@@Y?HHH@Z。这里给了个问号,意思就是不同的调用规则就更具调用规则表变化,其他不变。
很明显,C++的修饰更为详细。
现在回过头看看刚才的错误提示:error LNK2001: 无法解析的外部符号 __imp__ExitProcess@4
__imp_ 这个是代表函数ExitProcess是从外部导入的,后面的_ExitProcess@4很明显参数是四字节的和ExitProcess(UNIT uExitCode)相符
实际上对于C++的类成员函数,描述符的规则又有不同,但是,如果你写的DLL动态链接库使用自定义类,估计没人会用的,使用类了就不能通用了。
MASM的优化
都知道汇编效率高,但是MASM编译出的EXE真的就是最佳优化的么?让我们看看本章中的hello.exe 用OD反汇编看看是不是这样。
反汇编内容:
00011000 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00011002 |. 68 00300100 PUSH Hello.00013000 ; |Title = "A MessageBox !"
00011007 |. 68 0F300100 PUSH Hello.0001300F ; |Text = "Hello, World !"
0001100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0001100E |. E8 07000000 CALL
00011013 |. 6A 00 PUSH 0 ; /ExitCode = 0
00011015 /. E8 06000000 CALL
0001101A $- FF25 08200100 JMP NEAR DWORD PTR DS:[<&user32.Mess>; user32.MessageBoxA
00011020 .- FF25 00200100 JMP NEAR DWORD PTR DS:[<&kernel32.Ex>; kernel32.ExitProcess
看看那2个CALL,一个调用MessageBoxA,一个调用ExitProcess,这个JMP产生了额外的代码,并且增加执行时间,产生这样的代码 是因为编译器不知道你调用的函数是从外部导入的。如果编译器预先知道这个函数是从外部引入的,编译器就会把CALL后面的地址直接指向,PE文件的 IAT(import_address_table)输入表中的函数地址,当程序运行时由系统加载器更新IAT表(如果需要的话),这样就调用了函数在 DLL中的正确地址,避免了这种低效能的调用方式。
高级语言,比如C语言在引入外部DLL函数时,再dll头文件里对于每一个函数都有一个描述 __declspec(dllimport),这就是告诉编译器,这个函数是从外部引入的,从而提高空间和时间效率。
看看C写的,功能呢个同样的代码,编译后的反汇编内容:
00401000 /$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00401002 |. 68 00304000 PUSH HelloMsg.00403000 ; |Title = "HelloMsg"
00401007 |. 68 0C304000 PUSH HelloMsg.0040300C ; |Text = "Hello, Windows 98!"
0040100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0040100E |. FF15 AC204000 CALL NEAR DWORD PTR DS:[<&USER32.MessageBoxA>] ; /MessageBoxA
00401014 |. 33C0 XOR EAX, EAX
00401016 /. C2 1000 RETN 10
这个MessageBoxA的CALL才是效率最高的call!
但是悲剧的是在masm里我们无法用任何描述告诉编译器,当前使用的函数是从外部引入的。结果就是使用效率最高的语言确产生了效率最低的外部函数调用...
有没有办法解决,确实有,我google了一下,发现了一段代码。
比如我们调用ExitProcess函数,可以预先这样写
PROTO@4 TYPEDEF PROTO STDCALL :DWORD ;定义一个新的类型proto@4
EXTERNDEF STDCALL _imp__ExitProcess@4:PTR PROTO@4 ;定义一个外部变量,类型为上面定义的类型
ExitProcess EQU <_imp__ExitProcess@4> ;定义一个符号ExitProcess
把上面3行代码加到 模式定义后面,注释掉include 'kernel32.inc',重新编译,现在看反汇编的内容:
00011000 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00011002 |. 68 00300100 PUSH Hello.00013000 ; |Title = "A MessageBox !"
00011007 |. 68 0F300100 PUSH Hello.0001300F ; |Text = "Hello, World !"
0001100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0001100E |. E8 09000000 CALL
00011013 |. 6A 00 PUSH 0 ; /ExitCode = 0
00011015 /. FF15 00200100 CALL NEAR DWORD PTR DS:[<&kernel32.Ex>; /ExitProcess
0001101B CC INT3
0001101C $- FF25 08200100 JMP NEAR DWORD PTR DS:[<&user32.Mess>; user32.MessageBoxA
看见没ExitProcess的调用汇编代码成了最佳调用了。
新加的着3行代码,我也是网上抄下来的,请高手看见的帮忙解释下。
我还发现了一个网站 http://www.japheth.de/JWasm.html 这个网站提供了一套自己修改过的.inc文件,而且使用整个代码里只需要include 他们的windows.inc文件。
编译生成后就是优化了的call代码。
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.686
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
includelib user32.lib
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
szCaption db 'A MessageBox !',0
szText db 'Hello, World !',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
start:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
编译后的反汇编:
00401000 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00401002 |. 68 00304000 PUSH Hello.00403000 ; |Title = "A MessageBox !"
00401007 |. 68 0F304000 PUSH Hello.0040300F ; |Text = "Hello, World !"
0040100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0040100E |. FF15 08204000 CALL NEAR DWORD PTR DS:[<&user32.Mess>; /MessageBoxA
00401014 |. 6A 00 PUSH 0 ; /ExitCode = 0
00401016 /. FF15 00204000 CALL NEAR DWORD PTR DS:[<&kernel32.Ex>; /ExitProcess
2个call完全优化了。
不过要注意,用他们这个inc,不能使用vs2008自带的ml否则编译就报错,可以用RadASM里自带的masm 编译。之需要把参数/I 指向下载的win32inc的include就可以了。具体功能怎么实现的,我是不理解,还得请高手们帮忙看看。
windows下32位汇编语言学习笔记 第四章 第一个窗口程序 1 (windows的消息机制)
FirstWindow程序代码很简单,只有一个地方要说下
_WinMain 函数里的下面2行代码, 把当前进程句柄赋值给WNDCLASSEX的hInstance,这里不能使用mov @stWndClass.hInstance,hInstance,因为mov指令不支持2块内从间的直接赋值。
所以先把hInstance压栈再弹出到@stWndClass.hInstance
push hInstance
pop @stWndClass.hInstance
当然也可以这样做
mov ecx,hInstance
mov @stWndClass.hInstance,ecx
WIN32的消息机制
windows系统是一个消息驱动的OS,操作通过处理各种消息来响应用户的操作。从第一个windows程序就可以看出来,大部分的代码都是处理消息的。要开发windows程序,不管你用什么开发工具什么语言,掌握消息机制的原理都是非常必要的。
对于每一个带有窗口的线程,系统都会给他非配一个自己的消息队列,用于处理消息派送(Dispatch)。每个线程都用自己的消息循环来接受消息。每个线程列队默认管理最大10000个消息,修改注册表下面的键值可以修改列队中的消息数。建议的最小值是4000
HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Windows/USERPostMessageLimit.
线程列队不是一个公开的数据结构(THREADINFO),其中包括登记消息队列(Posted-message queue),消息发送队列 (Send-message queue),消息应答队列(reply-message queue),虚拟输入队列(virtualized- input queue),唤醒标志(wake flag),以及用来扫描线程局部输入状态的若干变量。(Windows核心编程)
消息列队提取优先级
1.检查QS_SENDMESSAGE 标志 GetMessage 不处理Send消息,如果队列中没有其他send消息,关闭QS_SENDMESSAGE标志,GetMessage()不返回检查其他消息。
2.检查QS_POSTMESSAGE 标志 GetMessage 从此列队取出消息处理并由DisPatch分发到指定窗口回调函数处理。GetMessage返回True,没有其他post消息关闭标志。
3.检查QS_QUIT 标志 如果被PostQuitMessage()打开,则GetMessage返回False退出消息循环,并且关闭QS_QUIT标志
4.检查QS_INPUT 标志 GetMessage 从此列队取出消息处理由TranslateMessage()处理键盘鼠标消息,然后由DisPatch分发到指定窗口回调函数处理没有其他消息关闭标志.
5.检查QS_PAINT 标志 处理同2
6.检查QS_TIME 标志 首先复位计时器,GetMessage返回True,如果没有计数器,关闭QS_TIME标志。
优先级很清楚,send优先级最高,最低的是time。
Windows定义了很多消息都以WM_开头,都是用#DEFINE 定义的常量,用户可以定义自己的消息,windows规定用户的消息从WM_USER 0x0400开始。
BOOL PostMessage(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam);
往进程的消息列队发送消息PostMessage,这个函数往指定进程的消息列队发送一个消息,发送完毕立即返回。调用函数无法知道发送的消息是否能被处理。如果这个指定窗口未处理完自己消息列队的所有消息前就推出了,就会处理不到post的消息。
PostMessage发送的消息参数不能包含指针参数,MSDN的说明是:
“如果发送一个低于WM_USER范围的消息给异步消息函数 (PostMessage.SendNotifyMessage,SendMesssgeCallback),消息参数不能包含指针。否则,操作将会失 败。函数将再接收线程处理消息之前返回,发送者将在内存被使用之前释放。”
我的理解是,就算目标进程知道你发来的是个指针地址,但是2个进程之间的寻址空间是独立的,互相不可访问,怎么能获取发送进程内存空间里的数据呢?
可以试下,把例子Send.asm修改一下看看。
WM_USER 值是 0x0400,用户自定义消息从这里开始,前面的是windows保留的消息。这里必须把WM_SETTEXT消息改成自定义消息,否则PostMessage根本不会发送WM_SETTEXT消息给目标线程。
invoke PostMessage,hWnd,WM_USER + 1,0,addr szText
Receive.asm修改接受WM_SETTEXT消息的代码改成这样:
eax == WM_USER + 1
invoke MessageBox,hWnd,addr lParam,addr szCaptionMain,MB_OK
用OD打开Receive.exe 运行在 0040102B处下段,运行send.exe,发送之前提示的地址(00402072)到了Receive,windows干脆直接把你传递的指针地址当做DWORD类型处理了,显示 "r @",
是00402072的ASCII码值,就算当成指针处理,00402072也是指向Receive内存空间里的,不可能是指向send内存空间里的数据。
关于WM_QUIT消息
窗口回调函数里不可能接到WM_QUIT消息。因为从消息列队里GetMessage()收到WM_QUIT消息 就返回0,消息循环就会结束,所以DispatchMessage()也不可能再把这个消息分发到窗口的回调函数。这就是为什么,书里一再强调要在 WM_DESTORY的消息事件里加上PostQuitMessage()的原因。如果不加,程序只是销毁窗口,但是进程任然存在。消息循环还在运行,但 是因为窗口已经销毁,所以他不可能再从消息列队里取得任何消息。
使用PostQuitMessage()与PostMessage()发送消息的不同
前者把消息列队里的QS_QUIT标志打开,并且等待程序处理完消息列队里的所有消息后才结束消息循环。
后者是把WM_QUIT直接放到消息列队,消息循环取到得下一个消息是WM_QUIT就立即退出。MSDN里不建议使用PostMessage发送WM_QUIT消息,因为这样会造成程序的收尾工作无法进行,正常退出后所需要的资源释放等操作就没法执行了。
用SendMessage无法发送WM_QUIT消息,因为SendMessage并不是吧消息放入消息列队,所以,GetMessage根本无法得到SendMessage发送的消息。
BOOL PostThreadMessage(DWORD dwThreadId,UINT uMsg,WPARAM wParam,LPARAM lParam);
这个函数和PostMessage类似,都是发送完消息立即返回,不同的是这个函数向指定的ThreadId发送 一条消息。这个函数发送的消息不回被分配到目标进程窗口的回调函数,因为当消息放入列队时,MSG的hwnd被设置为NULL,没有窗口句 柄,DispatchMessage能把消息分配给谁呢?PostThreadMessage也可以发送WM_QUIT消息,消息会放到队列的尾端。在 qs_input之前处理该消息。
PostMessage 和PostThreadMessage发送WM_QUIT消息都会造成窗口的首尾代码无法执行。用的时候需要注意下。
LRESULT SendMessage(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
SendMessage同步发送消息,发送进程要等待目标进程窗口的回调函数处理完成消息后才能恢复运行,调用点线程在等待SendMessage返回的过程中是挂起状态,本身也无法响应任何操作。
发送进程再等待的过程中,如果系统中其他的进程向等待进程发送消息,则发送进程立即处理消息。
Windows提供了其他的4个API来进行进程间的消息发送。
LRESULT SendMessageTimeout(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam,UINT fuFlags,UINT fuTimeout,PDWORD_PTR pdwResult);
fuFlags参数由下列标志组成
SMTO_ABORTIFHUNG 如果目标进程处于挂起状态立即返回。
SMTO_BLOCK 发送进程在SendMessageTimeout返回之前不处理任何消息
SMTO_NORMAL 0值,如果不适用其他标志,就是用这个标志
SMTO_NOTIMEOUTIFNOTHUNG 如果目标进程未处于挂起状态不考虑fuTimeout限定等待值
fuTimeout参数指定等待时间单位毫秒
pdwResult 指向一段内存区域,保存返回结果。如果用SendMessageTimeout本身线程的窗口则直接调用窗口的回调函数,并且将结果保存在pdwResult中。
BOOL SendMessageCallback(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam,SENDASYNCPROC lpCallback,ULONG_PTR dwData);
lpCallback 参数 指向一个CALLBACK函数,定义如下
VOID CALLBACK ResultBack(HWND hwnd,UINT uMsg,ULONG_PTR dwData,LRESULT lResult);
发送线程使用SendMessageCallback发送消息到接受线程的发送消息列队,并理解返回。当接收线程处理完消息后,用一个消息登记到发送线程 的应答消息队列,然后系统调用ResultBack函数通知发送进程。前2个参数是接受线程窗口的句柄,消息值,第三个参数dwData就是 SendMessageCallback中最后一个参数,lResult参数是接受消息窗口回调函数的返回值。
接收进程处理完SendMessageCallback函数后先在发送进程消息列队登记应答消息,发送进程在下一次调用GetMessage,PeekMessage时,执行ResultBack函数
Bool SendNotifyMessage(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam);
SendNotifyMessage将消息放到接收线程的发送消息列队(QS_SENDMESSAGE)中,并且立即返回。和PostMessage类似,但不同的是。
发送消息列队的优先级比登记列队(QS_POSTMESSAGE)的优先级高。所以SendNotifyMessage发送的消息比PostMessage发送的消息处理的早。
向进程发送窗口消息时,SendNotifyMessage效果和SendMessage完全一样,等待消息处理完之后才返回。
BOOL ReplyMessage(LRESULT lResult);
这个函数是用于接收线程窗口的回调函数中,调用ReplyMessage后,发送线程恢复运行。
判断消息类型
BOOL InSendMessage();
如果当前消息是进程间消息,返回TRUE,如果是进程内消息返回FALSE;
DWORD InSendMessageEx(PVOID pvReserved);
这个函数返回正在执行的消息类型。返回值如下:
ISMEX_NOSEND 消息是线程内部消息
ISMEX_SEND 消息是用SendMessage或SendMessageTimeout发送的进程间消息
ISMEX_NOTIFY 消息是SendNotifyMessage发送的进程间消息
ISMEX_CALLBACK 消息是SendMessageCallBack发送的进程间消息
ISMEX_REPLIED 消息是是进程间消息,并且已经调用ReplyMessage
消息队列的状态标志
DWORD GetQueueStatus(UINT fuFlags);
参数fuFlags是由一组标志联合起来的值,用来查看特定的唤醒队列标志。
QS_KEY WM_KEYUP、WM_KEYDOWN、WM_SYSKEYUP或WM_SYSKEYDOWN
QS_MOUSE MOVEWM_MOUSEMOVE
QS_MOUSEBUTTON WM_?BUTTON*(其中?代表L、M或R、*代表DOWN、UP或DBLCLK)
QS_MOUSE 同QS_MOUSEMOVE|QS_MOUSEBUTTON
QS_INPUT 同QS_MOUSE|QS_KEY
QS_PAINT WM_PAINT
QS_TIMER WM_TIMER
QS_HOTKEY WM_HOTKEY
QS_POSTMESSAGE 登记的消息(不同于硬件输入事件)。当队列在期望的消息过滤器范围内没有登记
的消息时,这个标志要消除。除此之外,这个标志与QS_ALLPOSTMESSAGE相同
QS_ALLPOSTMESSAGE 登记的消息(不同于硬件输入事件)。当队列完全没有登记的消息时(在任何消息
过滤器范围),该标志被清除。除此之外,该标志与QS_POSTMESSAGE相同
QS_ALLEVENTS 同QS_INPUT|QS_POSTMESSAGE|QS_TIMER|QS_PAINT|QS_HOTKEY
QS_QUIT 已调用PostQuitMessage。注意这个标志没有公开,所以在WinUser.h文件中没有。它由系统在内部使用
QS_SENDMESSAGE 由另一个线程发送的消息
QS_ALLINPUT 同QS_ALLEVENTS|QS_SENDMESSAGE
消息类型存放在回值的高字节中(2个字节),低字节储存还没有处理的消息类型。
上面几个函数都是用来发送消息,很多函数不是常用的,但多了解几个函数没有坏处,了解的东西越多,遇到问题解决的办法也就越多。
键盘,鼠标消息
windows程序与用户的互交都是通过鼠标键盘实现的,所以必须要了解windows是如何处理键盘鼠标消息的。
首先,发生的键盘鼠标消息是先报错在系统消息列队的(不是直接发放到应用程序列队),当应用程序处理上一个输入消息后,系统消息队列才把下一个输入消息投 放到应用程序列队。因为如果按键的输入速度比应用程序处理速度快,后来的键如果还是发往当前的焦点窗口句柄,那么切换到新窗口后后来输入的键还是会发送到 先前的窗口,直到上一个窗口处理完所有的未处理的按键消息,按键才会改变发送的窗口句柄到新窗口。
其次,每一个按键产生2类消息,按键消息和字符消息。很显然,有的按键只有按键消息没有字符消息,比如Capslk,Shift等。
按键又分为系统按键和非系统按键,对于系统按键,当按下一个键发生WM_SYSKEYDOWN消息,放开这个键发生WM_SYSKEYUP消息,对于非系统间,按下和放开发生WM_KEYDOWN和WM_KEYUP消息。
很显然这些消息都成成对出现的。一个KEYDOWN,接着就是一个KEYUP,
对于系统按键,通常是windows系统本身比较关心的消息,系统按键通常由ALT快捷键产生,Alt tab Alt F4 Alt esc 等等。应 用程序不处理ALT消息,而是交给DefWindowProc来处理,这就说明应用程序的菜单快捷键也是由系统处理。系统将Ctrl+s这类的快捷键,转 换成菜单命令消息,不用自己去处理。
对于所有的4类按键消息WM_SYSKEYDOWN WM_SYSKEYUP WM_KEYDOWN WM_KEYUP,wParam参数保存虚拟键代码,LParam参数包含按键的其他数据。
产生虚键代码的原因是因为早期的键码是由真实键盘产生,叫做"扫描码",扫描吗是按照键盘的排列顺序产生的,比如16 是Q,17是W(数数看,呵呵)很明显这种键码会因为键盘布局的变化而变化,太过于设备话,于是通过定义虚拟键代码。
虚拟代码是一些列VK开头的定义在winuser.h里的值。例如VK_TAB,VK_RETURN(回车键)等等,键盘数字0-9和字母a-z,A-Z 就是ASCII的值。小键盘上的数字是VK_NUMPAD0-VK_NUMPAD9,其他的功能键都是VK_+键的英文含义组成。
lParam参数的32位分成6个字段,用于表示不同的消息
00-15位, 包含按键的从重复次数。
16-23位, 包含按键的OEM扫描吗,上面说过扫描吗。
24 位, 包含按键的扩充标志,这个标准被windows忽略不用
29 位, 包含按键的内容代码,对于系统键盘此位是1,对于非系统键此位为0
30 位, 包含按键的先前状态,如果键是先前释放的,为0,否则为1.
31 位, 包含按键的转换状态,如果键盘按按下,为0,否则为1。
25-28位未知。
short GetKeyState(int vKey)函数用来获得某个键到目前为止的状态,比如判断shift是否按下 GetKeyState(VK_SHIFT),按下高位时1,否则是0,GetKeyState(VK_CAPITAL)(Capslk键)如果打开低位 返回1,注意这个GetKeyState返回short类型的值16位,不是上面说的LParam的值。GetKeyState不是实时检查状态的,指检 查到目前为止的键盘状态。
short GetAsyncKeyState(int vKey)函数用来获取当前的某个键的当前状态。高位为1则当前判断的建被按下,低位返回1则,则按键在上次调用GetasyncKeystate以来状态是被按下的。
GetKeyState判断组合键比较合适,因为可以判断某个键到目前为止的状态,按下了Ctrl再按下S,那么可以在S键的处理消息上判断GetKeyState(VK_LCTRL)是否按下。
GetAsyncKeyState可以用来做个循环,当某个键现在按下,处理某些事情。
字符消息
每当一个键被按下,就产生一个按键消息和字符消息,通常我们只关心字符消息,因为同样的按键产生的字符有可能是不同的,比如,打开搜狗输入法按Shit + 4打出的字符是¥,关闭输入法打出的是$。
字符消息的wParem参数是按键的ASCII值,所以在回调函数中可以if (wParam == 'a')这样判断输入的字符。
鼠标消息
鼠标按键全使用消息,每个键有3个消息,BUTTONDOWN,BUTTONUP,BUTTONDBLCLK(双击),WM_L(左键) WM_M(中键) WM_R(右键)加上三个消息代表了鼠标显示区消息。
鼠标的移动消息是WM_MOUSEMOVE
此时wParam参数表示下列的按键是否被按下MK_CONTROL MK_LBUTTON MK_MBMTTON MK_RBUTTON MK_SHIFT
lParam低位代表鼠标X坐标,高位代表鼠标Y坐标。
可以看出SendMessage()发送的WM_CHAR消息不会被目标进程的窗口回调函数处理,因为SendMessage直接发送到回调函数,没有经过TranslateMessage翻译键盘消息。
线程间的数据共享
WM_SETTEXT消息
首先说明WM_SETTEXT消息不是一个用来做进程间发送数据用的,这个消息是用来设置窗口标题,或者按钮文 本,或者Edit控件内容的。比如SetWindowText(HWND hwnd,LPCTSTR lpString)(设置窗口的标题)调用这个函数 实际上就是产生了一个WM_SETTEXT消息,通常由默认回调函数DefWindowProc来处理。想想就行了,只能发送一个字符串有什么用?
但是WM_SETTEXT消息特殊的地方就是,系统为用这个消息发送的字符串开辟另外一块共享内存映射空间,使不同进程接收消息的线程也能够收到并且使用这个字符串。
对应的还有一个WM_GETTEXT消息,这个消息是从目标窗口句柄返回字符串信息,同样 GetWindowText(HWND hwnd,LPCTSTR lpString,int iMaxCount)函数也是产生一个 WM_GETTEXT消息由DefWindowProc来处理,参数中多了一个iMaxCount用来表示字符串的长度。
WM_COPYDATA消息
WM_COPYDATA消息把自定义的一块数据发送到目标线程,目标进程的窗口回调函数中必须有这个消息的处理方法,否则发了也没用。
WM_COPYDATA WMSETTEXT这两个数据传递消息都只能使用SnedMessage()发送,SnedMessage返回了系统就会释放开辟的内存空间,用其他的方法发送,系统不知道目标进程什么时候处理消息,所以也无法释放内存映射空间。
可以在Send.asm里加入下面代码看看如果用PostMessage返回什么错误提示。
lpBuffer db 512 dup (?) ;先定义一个buffer
invoke PostMessage,hWnd,WM_COPYDATA,0,addr stCopyData ;试用PostMessage发送,根本就没有发送消息
.if eax == 0
invoke GetLastError
invoke FormatMessage,FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_IGNORE_INSERTS,NULL,
eax,LANG_NEUTRAL,offset lpBuffer,sizeof lpBuffer,NULL
invoke MessageBox,NULL,offset lpBuffer,offset szCaption,MB_OK
.endif
这个方法是使用GetLastError函数先获得上一次调用函数失败的代码,然后通过FormatMessage找到错误代码的描述,参数里设置说明是中文。
大部分的winAPI在调用失败后都可以通过GetLastError获得调用失败的错误代码。这个方法很好用,可以及时了解为什么出错。
本章关于汇编本省的东西不多,但是既然是windows的程序设计,必须先要了解一些windows程序设计一些基本的知识。特别是消息机制,这是整个windows程序设计的基础。
我参考了核心编程,windows程序设计,把windows的消息机制先做个介绍,了解了这些,再去看本章的代码和2个消息发送例子就轻车熟路了。
本章将分为2个部分做笔记,第一部分消息机制,第二部分准备做一些反汇编方面的实验,比如比较C写的窗口,和汇编写的窗口返回编的结果为何不同,比如如何找到并且拦截消息等等,当然这只是想法,能不能有结果还不一定。
键盘消息的使用
可以使用PostMessage给目标窗口或者控件发送键盘消息,按键消息和字符消息,但是使用SendMessage只能发送字符消息,而不能发送按键消息,想想为什么?
开始练习按键消息前,必须要先了解2个函数:
HWND FindWindow(LPCTSTR lpClassName,LPCTSTR lpWindowName);通过lpClassName窗 口注册类名(就是WNDCLASS里的lpszClassName名称)或者lpWindowName窗口标题名获得窗口句柄。
2个参数随便用一个就可以,不使用的给NULL。
HWND FindWindowEx(HWND,hwndParent,HWND hwndChildAfter,LPCTSTR lpszClass,LPSTSTR lpszWindow);这个函数可以通过窗口句柄和控件类名或者控件标题名获得这个控件的句柄。
先通过FindWindow得到主窗口句柄,然后通过FindWindowEx得到主窗口内某个控件的句柄。
下面看看如何通过PostMessage给windows记事本发送按键消息
首先找到记事本
szClac db 'Notepad',0 记事本主窗体的类名,可以通过Spy++获取
szEdit db 'Edit',0 内容用于写内容的Edit控件
hwndnote db ? 用于保存句柄
invoke FindWindow,offser szCalc,NULL ;找到记事本句柄
invoke FindWindowEx,eax,NULL,offset szEdit,0 ;找到edit控件的句柄
mov hwndnote,eax
下面就可以给记事本发送各种键盘消息,比如
invoke SendMessage,hwndnote,WM_KEYDOWN,VK_1,0 ;发送一个按键消息1
invoke PostMessage,hwndnote,WM_KEYDOWN,VK_2,0 ;发送一个按键消息2
invoke SendMessage,hwndnote,WM_CHAR,VK_3,0 ;用SendMessage发送一个字符消息3
想象发送后记事本上的的字符顺序是1,2,3么?
发送一个组合键Alt+E,就是打开记事本的编辑菜单
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_MENU,020000001h ALT键按下
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_E,020000001h E键按下必须要把第29位设置成1,代表alt键已经下
invoke PostMessage,hWndnd,WM_SYSCHAR,VK_E,020000001h 发送一个系统字符E
invoke PostMessage,hWndnd,WM_SYSKEYUP,VK_E,080000001h E键放开,必须把31位设置成1,表示这个是系统键
invoke PostMessage,hWndnd,WM_KEYUP,VK_MENU,080000001h ALT键放开,31位系统键设置成1
这组消息可以通过SPY++监视记事本的键盘输入状态得到,其实可以精简,只用下面2条就可以。
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_E,020000001h E键按下必须要把第29位设置成1,代表alt键已经下
invoke PostMessage,hWndnd,WM_SYSKEYUP,VK_E,080000001h E键放开,必须把31位设置成1,表示这个是系统键
因为E键的lParam参数的29位置1,已经说明这个E在这里表示系统按键,29位置1表示ALT键已经按下。
按键弹起的时候,必须把31位置1,表示这是个系统键弹起。否则会当做普通键,并且在记事本里打印出字母e。
现在想出来这组消息后,记事本上会是什么字符么?答案是:321,前面说过SendMessage的优先级高于PostMessage,所以是先打出3,然后是1,最后是2。
关于windows消息的操作还有很多,这里只举出了最基本的发送键盘消息的方法。理解这些基本的操作是位日后学习使用其他消息操作打下一个好的基础。
鼠标消息的使用
键盘消息只发送给当前拥有输入焦点的窗口,鼠标消息不同,只要鼠标达到,窗口就会收到鼠标消息。当鼠标在窗口显示区域内,鼠标消息的lParam参数是鼠标所在窗口的X,Y坐标值,当鼠标不在窗口显示区域内,参数lParam是桌面的X,Y坐标值。
显示区域:是指用户能够输出显示信息结果的区域。非显示区域是指:菜单,标题栏,滚动条
对于显示区内发送鼠按键消息,wParam参数指定鼠标按键以及Shift和Ctrl按键的状态,键值如下:
MK_CONTROL 表示ctrl按下 MK_?BUTTON 表示鼠标3个键按下 MK_SHIFT 表示shift按下
lParam参数指定鼠标的坐标值,高位Y坐标,低位X坐标
下面的例子代码是使用键盘的上下左右方向键移动鼠标光标,空格键发送鼠标单击消息。可以把SendMessage句柄改成“画图”程序句柄,这样在当前窗口按空格键,将会在画图程序的同样位置画出一个点。
_MoveMouse proc hwnd,wParam,lParam
local @szPos [128]:byte
local @stPoint:POINT
local @stRect:RECT
invoke GetCursorPos,addr @stPoint ;获得当前鼠标屏幕坐标位置
invoke ScreenToClient,hwnd,addr @stPoint ;将鼠标的屏幕坐标位位置转换成当前窗口内的坐标位置
invoke wsprintf,addr @szPos,offset szMsg,@stPoint.x,@stPoint.y
invoke SetWindowText,hwnd,addr @szPos
mov eax,wParam
.if eax == VK_LEFT
sub @stPoint.x,1
.elseif eax == VK_RIGHT
add @stPoint.x,1
.elseif eax == VK_UP
sub @stPoint.y,1
.elseif eax == VK_DOWN
add @stPoint.y,1
.elseif eax == VK_SPACE
mov eax,@stPoint.y
shl eax,16
add eax,@stPoint.x
invoke PostMessage,hwnd,WM_LBUTTONDOWN,MK_LBUTTON,eax
invoke PostMessage,hwnd,WM_LBUTTONUP,0,eax
.endif
invoke ClientToScreen,hwnd,addr @stPoint ;将当前窗口坐标位置转换成屏幕位置
invoke SetCursorPos,@stPoint.x,@stPoint.y ;设置光标位置
ret
_MoveMouse endp
在窗口的回调函数中加入以下代码:
.elseif eax == WM_KEYDOWN
mov eax,wParam
.if wParam == VK_LEFT || wParam || VK_RIGHT || wParam == VK_UP || wParam == VK_DOWN || wParam == VK_SPACE
invoke _MoveMouse,hWnd,wParam,lParam
.endif
对于非显示区鼠标消息和显示区鼠标消息类似,消息后加"NC"代码表示非显示区消息,例如WM_NCLBUTTONCLICK
参数wParam是一些定义在winuser.h里以HT开头的的非显示区域代码,比如HTCAPTION 代表标题栏,HTCLOSE,代表窗口右上角的关闭按钮等等。
参数lParam表示屏幕坐标,不是显示区坐标,同样低位是X坐标,高位是Y坐标。
纯C写的FirstWindow和汇编FirstWindow的区别
同样的FirstWindow程序,我用C写了一个,反汇编后比较,发现反汇编结果里多了很多编译器添加的代码。尺寸也大了不少,查了一些资料,发现原来这些编译器添加的代码就是传说中的CRT,C语言运行时环境。
用C写windows程序,都知道程序从winMain开始执行,实际上在这之前,是有其他的函数来调用WinMain的。这个函数就叫做入口函数。
入口函数对运行时库和程序运行环境进行初始化,包括堆,I/O,线程等等。入口函数执行完后才回去调用main函数正式开始执行程序,WinMain执行完后,返回到入口函数,由入口函数进行清理工作。
这倒也好理解,winMain之前肯定有些东西执行了什么,比如winMain的4个参数,hInstance,szCmdLine,iCmdShow 都是从启动函数传给winMain的。
对于我现在使用的vs2008的编译器来说,入口函数的代码位于srt/src/crt0.c文件里。函数的名称是__tmainCRTStartup。现在看看里面都干了些什么关键:
首先定义了个STARTUPINFO StartupInfo结构,使用GetStartupInfo(&StartupInfo)初始化。STARTUPINFO结构包含一些进程的信息。具体细节可以查看msdn.
紧接着初始化堆 _heap_init(1)
初始化堆是很重要的,否则不能使用C++的new 或c的malloc来分配内存。
然后初始化多线程 _mtinit()
然后初始化I/O,_ioinit(),得到命令行参数GetCommandLineT();得到当前进程进程版本信息
最后调用启动函数
WinMain((HINSTANCE) & __ImageBase,NULL,lpszCommandLine,StartupInfo.dwFlags & STARTF_USESHOWWINDOW? StartupInfo.wShowWindow: SW_SHOWDEFAULT);
到这里就可以看见,winMain的参数是怎么来的了,hInsteance 就是__ImageBase(载入基址),命令行参数也是传进来的,最后的iCmdShow,参数就是STRTUPINFO里的显示方式。
就是因为编译时加入了启动函数所以使C程序编译出来的可执行文件比汇编程的大了30多K。
其实启动函数不是必须的,可以自定义一个自己的启动函数代替默认的启动函数。
比如定义一个
int WINAPI main()
{
HINSTANCE hInstance = GetModuleHandle(NULL); //得到当前进程的句柄,和汇编一样
LPSTR lpszCmdLine = GetCommandLine(); //获得命令行参数
int r = WinMain(hInstance, NULL, lpszCmdLine, SW_SHOWDEFAULT); //调用WinMain函数,就开始执行
ExitProcess(r); //最后结束进程
return r; // this will never be reached.
}
需要在link.exe 后加/entry:main /nodefaultlib:msvcrt90.lib参数,/entry指定入口点函数, /nodefaultlib指定不连接运行时库。
这样编译连接后,可执行文件尺寸和汇编后的大小一样。反汇编后比较内容也基本差不多。要不说C语言执行速度快,编译后的内容和直接用汇编写的程序基本上一样。
windows 下32 位汇 编语 言学 习笔 记 第十章 内存管理部分
前面5 6 7 8 9章都是介绍windows 界面设计有关的内容,这些内容大概看一下就可以,等需要用的时候再回过头来查。一次性记 住的可能性不大。这些章节也没有什么难度,自己看看就可以。
我所关心的还是windows 系统相关知识,说道界面设置,对RC资源再熟悉做界面还是Delphi,C++builder比较快速。
本章介绍了很多windows 下关于内存管理的函数,书上有句话说的很好,不要去深究这些函数分配的内存具体放在内存寻址空间的什么地方,只需要知道什么时候该用什么类型的内存管理函数就可以。
Global标准内存管理
GlobalAlloc函数使用GMEM_MOVEABLE参数返回的是个内存句柄,内容是一个地址,这个地址才是可以使用的内存块。比如:
PVOID lpMem = GlobalAlloc(GHND, 1000);
现在lpMem指向的内容是一个地址,必须通过GlobalLock(lpMem)函数返回这个指针才能使用。
实际上GMEM_MOVEABLE参数和GMEM_FIXED区别就是前者必须要经过GlobalLock才能使用,后者则通过返回的指针直接使用。
关于什么时候可移动,我没法测,不知道windows 在什么情况下去移动这块内存。
GlobalAlloc分配内存是紧凑的,就是说分配的多个内存空间可以放在一个内存页面里,只要能放的下。
Heap堆内存管理
Heap内存管理非配基本上和Global的用法相当,一般的用法都是GetProcessHeap得到进程默认堆,然后HeapAlloc分配。
虚拟内存管理
有一点必须知道,VirtualAlloc,所分配的空间都是页对齐的,分配1字节的内存空间也会占用4096字节的一个内存页,再次使用VirtualAlloc分配内存会从新的一页开始。
每个虚拟内存页面有三种状态:Free:自由状态,Commit:提交状态,Reserve:保留状态。
八种保护属性:
PAGE_NOACCESS,PAGE_READONLY,PAGE_READWRITE,PAGE_EXECUTE,PAGE_EXECUTE_READ,PAGE_EXECUTE_READWRITE,PAGE_EXECUTE_WRITECOPY,PAGE_WRITECOPY
这八种属性在第一章的笔记 里有介绍。
虚拟内存页面还有4种类型
Free:空闲,没有提交也没有使用的。Private:私有,该区域的虚拟地址和系统页文件关联。Image:映像,改区域的虚拟内存地址和exe文件或dll文件关联。
Mapped:映射,改区域的内存和其他硬盘上的文件关联。
这些页面的属性,类型,状态,就是80386保护模式下,windows 系统管理虚拟内存的机制。
虚拟内存管理函数中提供了几个可以操作其他进程虚拟地址的函数。
LPVOID VirtualAllocEx(HANDLE hProess,LPVOID lpAddress,SIZE_T dwSize,DWORD flProtect)
这个函数可以在别的进程空间保留或者提交一块内存。比如DLL的远程注入就是使用这个函数在目标进程开辟一块虚拟内存空间,将要注入的dll名通过 WriteProcessMemory函数写进目标进程的虚拟内存空间,然后再通过CreateRemoteThread函数运行 LoadLibraryA加载写入的dll文件,LoadLibraryA函数位于kernel32 .dll中,由于每个进程运行都会加载这个dll,所以LoadLibraryA函数在不同进程中的地址是一样的,可以直接再目标进程使用。
比如下面的例子:
// 向目标进程地址空间写入DLL名称
DWORD dwSize, dwWritten;
dwSize = lstrlenA( lpszDll ) + 1;
LPVOID lpBuf = VirtualAllocEx( hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE );
if ( NULL == lpBuf )
{
CloseHandle( hProcess );
// 失败处理
}
if ( WriteProcessMemory( hProcess, lpBuf, (LPVOID)lpszDll, dwSize, &dwWritten ) )
{
// 要写入字节数与实际写入字节数不相等,仍属失败
if ( dwWritten != dwSize )
{
VirtualFreeEx( hProcess, lpBuf, dwSize, MEM_DECOMMIT );
CloseHandle( hProcess );
// 失败处理
}
}
else
{
CloseHandle( hProcess );
// 失败处理
}
// 使目标进程调用LoadLibrary,加载DLL
DWORD dwID;
LPVOID pFunc = LoadLibraryA;
HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID );
___________________________________________________________________________________________________________________________
DWORD VirtualQuery(PVOID pvAddress,PMEMORY_BASIC_INFORMATION lpBuffer,SIZE_T dwLength);
这个函数可以用来查询pvAddress指定的内存空间的状态State,类型Type,和保护属性Protect,地址空间的属性保存在 MEMORY_BASIC_INFO结构中。用这个函数,就可以写一个和Cheat Engine里一样的查看内存Region的程序。
下面这个小程序就是使用VirtualQuery函数实现查询当前进程中的内存区域状态。实现的功能比Cheat Engine里的还多了可以知道哪里的内存是进程堆,那里映射,映像了什么文件。
部分参考windows 核心编程第10章的内容
;显示内存信息
.386
.model flat,stdcall
option casemap:none
include windows .inc
include kernel32 .inc
includelib kernel32 .lib
include macros.asm
include user32 .inc
includelib user32 .lib
include Psapi.inc
includelib Psapi.lib
IDD_DIALOG equ 104
IDC_LIST equ 1001
.data?
hInstance dd ?
hWinMain dd ?
hProcess dd ?
.const
msg db "%10p %15s %15s %10p %10s",0
memfree db "MEM_FREE ",0
memcommit db "MEM_COMMIT ",0
memreserve db "MEM_RESERVE",0
unknow db ' ',0
memimage db "MEM_IMAGE ",0
memmapped db "MEM_MAPPED ",0
memprivate db "MEM_PRIVATE",0
.code
;内存状态
_FormatState proc wState
mov eax,wState
.if eax == MEM_COMMIT
lea eax,offset memcommit
.elseif eax == MEM_FREE
lea eax,offset memfree
.elseif eax == MEM_RESERVE
lea eax,offset memreserve
.else
lea eax,offset unknow
.endif
ret
_FormatState endp
;内存类型
_FormatType proc wType
mov eax,wType
.if eax == MEM_IMAGE
lea eax,offset memimage
.elseif eax == MEM_MAPPED
lea eax,offset memmapped
.elseif eax == MEM_PRIVATE
lea eax,offset memprivate
.else
lea eax,offset unknow
.endif
ret
_FormatType endp
;保护属性
_FormatProtect proc wProtect,szBuffer
mov eax,wProtect
.if eax == PAGE_READONLY
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT('-R--')
.elseif eax == PAGE_READWRITE
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("-RW-")
.elseif eax == PAGE_WRITECOPY
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("-RWC")
.elseif eax == PAGE_EXECUTE
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("E---")
.elseif eax == PAGE_EXECUTE_READ
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("ER--")
.elseif eax == PAGE_EXECUTE_READWRITE
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("ERW-")
.elseif eax == PAGE_EXECUTE_WRITECOPY
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("ERWC")
.else
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("----")
.endif
ret
_FormatProtect endp
;显示内存信息
_ShowMemoryState proc uses ecx hwndLB
local @msg[1024]:byte
local @path[MAX_PATH]:byte
local @mbi:MEMORY_BASIC_INFORMATION
local @Ret:dword
local @szState:dword
local @szType:dword
local @szProtect[5]:byte
local @pHeapAddress:dword
;ebx作为下一个查询的内存地址
xor ebx,ebx
_loopbegin:
invoke VirtualQuery,ebx,addr @mbi,sizeof @mbi
mov @Ret,eax
mov eax,@mbi.BaseAddress
invoke _FormatState,@mbi.State
mov @szState,eax
invoke _FormatType,@mbi.lType
mov @szType,eax
invoke _FormatProtect,@mbi.Protect,addr @szProtect
invoke wsprintf,addr @msg,offset msg,@mbi.BaseAddress,@szState,@szType,@mbi.RegionSize,addr @szProtect
;标志内存映射,映像文件
.if @mbi.State != MEM_PRIVATE
invoke GetCurrentProcess
mov hProcess,eax
invoke GetMappedFileName,hProcess,ebx,addr @path,MAX_PATH
.if eax
invoke lstrcat,addr @msg,CTXT(' ')
invoke lstrcat,addr @msg,addr @path
.endif
.endif
;标志堆地址
invoke GetProcessHeap
mov @pHeapAddress,eax
.if ebx==eax
invoke lstrcat,addr @msg,CTXT(' ')
invoke lstrcat,addr @msg,CTXT('Process Heap Address')
.endif
invoke SendMessage,hwndLB,LB_ADDSTRING,0,addr @msg
add ebx,@mbi.RegionSize
cmp @Ret,sizeof @mbi
je _loopbegin
ret
_ShowMemoryState endp
_DlgProc proc hWnd,wMsg,wParam,lParam
mov eax,wMsg
.if eax == WM_CLOSE
invoke EndDialog,hWnd,0
.elseif eax == WM_INITDIALOG
invoke GetDlgItem,hWnd,IDC_LIST
invoke _ShowMemoryState,eax
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_DlgProc endp
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_DIALOG,NULL,offset _DlgProc,WM_INITDIALOG
invoke ExitProcess,NULL
end start
//资源文件 使用ResEdit编辑
// Generated by ResEdit 1.4.4.19
// Copyright (C) 2006-2008
// http://www.resedit.net
#include "res.h"
#include <windows .h>
#include
#include
//
// Dialog resources
//
IDD_DIALOG DIALOGEX 0, 0, 535, 252
STYLE DS_MODALFRAME | DS_SETFONT | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP | WS_SYSMENU
EXSTYLE WS_EX_WINDOWEDGE
CAPTION "Dialog"
FONT 10, "Courier New", 400, 0, 0
BEGIN
LISTBOX IDC_LIST, 0, 0, 535, 250, WS_TABSTOP | WS_VSCROLL | LBS_NOINTEGRALHEIGHT | LBS_SORT | LBS_NOTIFY
END
通过循环调用VirtualQuery,通过返回的mbi结构获得所有进程地址空间的各种信息,从0地址开始查找,每次查找后把地址加上mbi结构的RegionSize(区域尺寸)再查找下一个区域的信息。
还可以通过DWORD GetMappedFileName(HANDLE hProcess,LPVOID,lpv,LPTSTR lpFilename,DWORD nSize)检查类型为MEM_IMAGE,MEM_MAPPED的内存区域加载文件的路径。
还可以通过HANDLE GetProcessHeap(VOID)获得进程堆地址。
程序里用到了Masm宏CTEXT(),用这个宏就不必每次使用个字符串都去.data或.const里定义,这个宏实际上就是实现再.data 或者.const定义一个字符串。
程序的运行结果如下:
依次显示:Address,State,Type,RegionSize,Protect,其他描述
汇编 的串指令
汇编 中有一些专门用来处理连续内存单元的指令,叫做串操作指令。串指令通过EDI或ESI来指定,可以对内存单元按字节,字或者双子进行处理,并更具操作对象的字节数根据DF(方向标志)对DEI,ESI变址寄存器自动增减1,2,或4字节.
先看看书里的例子:
将szSource中的内存移动到szDest中.
mov esi,offset szSource
mov edi,offset szDest
mov ecx,dwSize
cld
rep movesb
cld指令是清方向指令,使DF标志为0,使每次移动一个字节,esi和edi都加1。
movesb 是串移动指令,后面的b代表byte每次移动一个字节类型,还可以是movesw(移动一个字),movesd(移动一个双字),并根据方向位和字节大小对dsi和edi进行增减.
rep是重复字符串操作指令,后面跟一个串操作指令来重复串指令,重复的次数由ecx来决定,所以,上面例子中把dwSize放到ecx寄存器中,用来指定内存单元大小。
stosb/stosw/stosd指令的作用是把al(1字节),ax(2字节),eax(4字节)的值,填充EDI指向的内存区域。同样根据DF来对EDI进行增减。
lodsb/lodsw/lodsd指令是把ESI指向的内存区域取一个字节,2字或者4字节,放入al,ax,eax中,根据DF标志位对ESI进行增减。
本章的内容需要熟练掌握,不管写什么样的程序,对可用内存的利用的和理解都是很关键的。对于本章,我计划多写一些小程序,1来加深对内从使用的理解,2来熟悉汇编 语法。
由于好久没有跟新,所以先发上来一部分,第二部分会有更多的内存使用的代码和例子。
windows 下32 位汇 编语 言学 习笔 记 第十章 内存管理部分 2
这两天通过写汇编 程序,越来越发现汇编 很有意思。自己规划每一个寄存器的使用,设计每一个跳转和分支,这不同于使用其他高级语言 ,所有资源对于编程者都是透明的,让我有一种尽在掌握的感觉,而且每写一个程序都很有成就感,这是我用别的语言 写程序所没有的感觉。
不管学习 什么东西,实践是最重要的,计算机程序设计这种实践性很强的科目更是如此。有的东西看似简单,实际动起手来可就不那么容易了,所以必须要告诫大家,学习 计算机程序设计,必须要勤动手,不能懒惰。如果你能够把 windows 程序设计,windows 核心编程,windows 32 位汇 编语 言,这3本书的所有例子自己用汇编 写一遍,我可以很负责的告诉你,你已经是高手了。
言归正传,笔记 继续
汇编 的跳转,分支,循环指令
在继续程序之前,我觉得有必要把汇编 的跳转,分支,循环指令总结一下,有一点必须要清楚,我们现在的目的是学习 汇编 ,为将来的更深入的学习 逆向打下良好的基础。这两天在写程序的过程中,我发现我背离了我的初衷,看看以前我的代码例子,完全是用C程序的思路换成汇编 语法,包括罗云彬这本书里的例子程序也是如此。大量的条件判断使用masm伪指令,比如.if,虽然使用这种伪指令的汇编 程序更利于阅读,结构更加清晰,但是,我感觉根本没有学到汇编 的精髓,或者说没有立即汇编 的真谛。
标号:
标号的定义是,代码中的某个具体位置。
在我们的源代码中,标号就好比书签,让我们设计分支,循环语句时引导程序的运行流程。在编译器中,标号的意义在于标志处跳转指令和目的地址的范围,用以计算这段范围内的字节数,用于生成机器码。
为什么我这么理解,用jmp指令举个例子,先看看下面的代码,这是一个典型的Dialog窗口回调函数。
_DlgProc proc hwndDlg,uMsg,wParam,lParam
mov eax,uMsg
cmp eax,WM_COMMAND
jne _init
invoke _DlgCmd,hwndDlg,wParam,lParam
jmp _ret
_init: ;标记 处理init消息
cmp eax,WM_INITDIALOG
jne _close
invoke LoadIcon,hInstance,IDI_VMALLOC
invoke SendMessage,hwndDlg,WM_SETICON,ICON_BIG,eax
jmp _initret
_close: ;标记 处理close消息
cmp eax,WM_CLOSE
jne _ret
invoke EndDialog,hwndDlg,0
jmp _ret
_initret: ;对于WM_INITDIALOG消息,处理完成后必须返回1
mov eax,TRUE
_ret: ;标记 返回
mov eax,FALSE
ret
_DlgProc endp
这是一个正真的(指不用伪指令)汇编 语言 程序,里面用到得其他转移以后再说,现在先看jmp指令,刚才我说了,在我们的源代码里,标号就好比书签的作用,通过标号,我们指定程序的运行流程。
再看看这段程序反汇编 以后的内容,先只关注里面的jmp指令.
00401487 /. 55 PUSH EBP
00401488 |. 8BEC MOV EBP, ESP
0040148A |. 8B45 0C MOV EAX, DWORD PTR SS:[EBP+C]
0040148D |. 3D 11010000 CMP EAX, 111 ; Switch (cases 10..111)
00401492 |. 75 10 JNZ SHORT MyVMAllo.004014A4
00401494 |. FF75 14 PUSH DWORD PTR SS:[EBP+14] ; /Arg3; Case 111 (WM_COMMAND) of switch 0040148D
00401497 |. FF75 10 PUSH DWORD PTR SS:[EBP+10] ; |Arg2
0040149A |. FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |Arg1
0040149D |. E8 47FEFFFF CALL MyVMAllo.004012E9 ; /MyVMAllo.004012E9
004014A2 |. EB 3C JMP SHORT MyVMAllo.004014E0
004014A4 |> 3D 10010000 CMP EAX, 110
004014A9 |. 75 1F JNZ SHORT MyVMAllo.004014CA
004014AB |. 6A 65 PUSH 65 ; /RsrcName = 101.; Case 110 (WM_INITDIALOG) of switch 0040148D
004014AD |. FF35 F4304000 PUSH DWORD PTR DS:[4030F4] ; |hInst = NULL
004014B3 |. E8 9A000000 CALL
004014B8 |. 50 PUSH EAX ; /lParam
004014B9 |. 6A 01 PUSH 1 ; |wParam = 1
004014BB |. 68 80000000 PUSH 80 ; |Message = WM_SETICON
004014C0 |. FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hWnd
004014C3 |. E8 90000000 CALL
004014C8 |. EB 11 JMP SHORT MyVMAllo.004014DB
004014CA |> 83F8 10 CMP EAX, 10
004014CD |. 75 11 JNZ SHORT MyVMAllo.004014E0
004014CF |. 6A 00 PUSH 0 ; /Result = 0; Case 10 (WM_CLOSE) of switch 0040148D
004014D1 |. FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hWnd
004014D4 |. E8 67000000 CALL
004014D9 |. EB 05 JMP SHORT MyVMAllo.004014E0
004014DB |> B8 01000000 MOV EAX, 1
004014E0 |> B8 00000000 MOV EAX, 0 ; Default case of switch 0040148D
004014E5 |. C9 LEAVE
004014E6 /. C2 1000 RETN 10
先看第一条jmp指令004014A2 |. EB 3C JMP SHORT MyVMAllo.004014E0,也就是源代码中的jmp _ret。
可以看到,真正编译后,可执行程序里根本没有我们定义的标号,而是直接替换成了一个地址004014E0,把我们代码里的_ret换成里一个地址,让我们看看原理。
在编译程序的时候,编译器负责把汇编 源代码翻译成机器码(操作码),操作码 都是16进制的数据类型,比如jmp指令的硬件码有2个,E9(near跳转) 和 EB(short跳转)看看第一条jmp指令,硬件码是 EB 3C,EB就代表jmp指令,3C是什么?3C就是指令地址到目标地址的一个偏移量,也就是中间这段区域的字节大小。这段距离字节的大小可以这样计 算。
偏移量 = 目标地址-跳转指令地址-跳转指令字节数 = 004014E0 - 004014A2 - 2 (EB 3C2个字节) = 3C
就是通过这样的计算,编译器把jmp _ret代码翻译成了EB 3C 操作码。把我们源代码里的标号语句替换成了实际的目的地地址,总不能让程序员自己去计算跳到那里需要多少字节把。
注:所有的跳转指令都有near跳转和short跳转2种,short跳转(也叫近跳转)指跳转距离在127(0x7F)字节以内,0x7F是1字节的16进制所能表达的最大的正数,再大就是负数了0x80,就成了-128了。
near跳转(也叫长跳转)范围是0x7FFFFFFF之内,就是4字节16进制所能表达的最大正数。
所以对于进跳转,上面计算偏移量的的指令本身长度就是EB+1字节的跳转范围,共2字节,对于元跳转就是E9+4字节的跳转最大范围,共5字节。
汇编 的分支,循环,在代码中都是通过标号来确定指令的转移的具体位置,所以必须先要理解标号的作用。
汇编 的条件分支
汇编 的分支简单的理解就是高级语言 中的if else,与高级语言 不通的是,汇编 的条件分支将高级语言 中的if else细化了。看看为什么说是细化了。
比如C语言 的if例子:
if(100 < 200)
...
else
...
这个if实际上计算机要做很多工作,分解来看。
1.首先要比较100 < 200 是否成立。
2.如何比较?是用100-200判断得出是否是负数,还是用200-100判断是否是正数?
3.通过上面的2种比较方法的不同答案,确定是继续执行还是跳转到else后面执行。
实际上这个if里的最关键的地方第二步中用什么方式判断100<200,以及转移方法,在高级语言 中我们根本不去考虑,也从没考虑过。
标志寄存器
继续学习 分支前,先来了解一下汇编 中的几个标志寄存器flags register(EFLAGS),下面看看这个寄存器中的“位”于“标志”的关系。
第0 位 CF(Carry Flag) 进位标志位 | 第2 位 PF(Parity Flag)奇偶标志位 | 第6 位 ZF(Zero Flag) 零标志位 | 第7位 SF(Sign Flag) 符号标志位
第10位 DF(Direction Flag) 方向标志位 | 第11位 OF(OverFlow Flow) 溢出标志位
根本不用背,理解了为什么需要这些标志位,你自然就会记 住这些标志位。
其中的CF OF SF ZF 四个标志是与条件分支指令息息相关的,这些条件指令通过对条件运算后所产生的标志位来确定如何跳转。
还是用上面的if(100 < 200)来理解标志寄存器,首先需要计算100<200这个表达式,如果用脑袋想,估计会像下面这样:
1.用100-200,等到一个值-100
2.判断-100是是等于0还是不等于0。(计算机里0代表假,其他数代表真)
3.如果等于0,哦,执行某某地方,如果不等于0,哦,执行某某地方。
实际上成了一个运算,2个判断。
看看计算机是如何处理的,先用汇编 来重写这个判断
cmp 100,200
jge else 大于等于跳转
...
else:
...
1.首先也是用100-200。100-200=-100 那么标志寄存器的SF就被置1因为是负数
2.计算机不去理会结果是多少,而是看寄存器中的标志位。如果SF是1,则说明第一个数比第二个数小,就直接跳转。
既不用保存计算结果,也不用把结果再和0比较。计算后通过标志位就知道该如何跳转,这就是汇编 的条件跳转指令的执行方式。
条件转移指令分为有符号的和无符号的。
有符号的条件转移指令通过标志寄存器的SF标志来判断是否跳转,而无符号的条件转移指令通过CF标志来判断是否跳转,还有一些条件转移指令通过ZF标志判断跳转。
所有的跳转前都有会有一条指令来改变这些标志位,通常使用cmp 操作数1,操作数2,通过操作数1-操作数2,来改变标志位。条件转移指令紧跟在cmp指令后面进行跳转。
条件转移指令
所有条件转移指令都以J开头后面跟然后是条件或者标志的英文缩写。
Equal(等于) Above,Greater(大于) Below,Less(小于) Not(非),C,O,S,Z(CF,OF,SF,ZF四个标志)
如果有not则n在条件缩写的前面,下面对照高级语言 的比较来看汇编 的条件指令。以下面的if做模板。
if(a>b) cmp a,b
... jl/jb _else
... ...
else _else
... ...
if(a>=b) 汇编 : jnl/jnb _else
if(a if(a<=b) jng/jna _else
if(a==b) jne/jnz _else
if(a!-b) je/jz _else
所有的条件指令全是和高级语言 中的判断符号相反,判断> 指令用jl 不小于,判断<,指令用jg不大于,这是因为当cmp指令执行后,当前的标志寄存器的状态就是cmp指令 操作数1 - 操作数2
所产生,必须在其他指令该表标志位前进行条件转移。
Less(小于),Greater(大于)是对有符号数使用的,Above(大于),Bolow(小于)是对无符号数使用的。
cmp eax,100
ja _big 那么只要EAX的16进制值大于100,就会跳转。00000065-FFFFFFFF ,不判断符号位,大于就跳转。
jg _big 那么只有EAX里的值大于100,而且不为负数的时候才跳转。00000065-7FFFFFFF ,正数范围内,判断大于。
其实很简单,当你写汇编 代码cmp x,y 的时候,下一句的条件转移指令必须是条件不成立时的转移地址。所以反着来写就Ok。
汇编 的循环指令
理解了上面的标号,跳转指令,和条件转移指令,就能够写出任何的高级语言 中的循环。无非就是这些指令的合理组合。
while()循环
_while: cmp a,b
j?? _endwhile ;不成立了就跳到结束
...
jmp _while ;跳到_while处继续比较
_endwhile:
汇编 还有一种简单的循环方式,就是loop,loop指令使用ecx作为计数器,每次执行到loop,ecx将自动-1,知道ecx为0时退出循环。比如:
mov ecx,100 ;循环100次
_for:
... ;循环体
loop _for ;循环100次,直到ecx被减到0,停止循环
loop还有loope,loopne,两个指令,用来判断当循环体内某一个条件成立则退出循环。
mov ecx,100 ;循环100次
_for:
... ;循环体
cmo ecx,3
loopne _for ;如果ecx不等于3才继续循环,也就是只循环97次。
汇编 的条件指令和高级语言 中的条件指令相比,需要关注更多的细节,由于标号的使用,对于程序结构的设计就需要更加小心和细致,否则不仅容易出错还会造成难以维护的后果。唯一的熟练掌握的方法就是,多写,多练,多看别人的程序(最简单的就是反汇编 自己用C或者C++写的循环,判断)看看编译器是如何组织的。
最后,贴上我写的一个虚拟内存应用的一个小例子,这个小程序是我结合windows 核心编程中,第15章的例子设计的,见图:
Alloc Num用来输入需要保留多少个页面文件。保留后,使用use提交在Index后面Edit文本框输入的值(0<=值< Alloc Num)的这块内存页。Clear用来释放指定值的内存页,Clear All释放所有提交的内存。下面的Memory View查看分配内 存的页面信息。每次提交,清除后都会刷新显示。
上传的附件
15-VMAlloc.rar (2.7 KB, 18 次下载) | [谁下载? ] |