Win32汇编笔记-程序框架

 

基本概念

  Win16 内存模式下,所有的应用程序都运行于同一个 4GB 地址空间,它们可以彼此"看"到别的程序的内容,这极易导致一个应用程序破坏另一个应用程序甚至是操作系统的数据或代码。

  每一个Win32 应用程序,放到分开的虚拟地址空间(相互独立的 4GB 地址空间)中去运行,当然这倒不是说它们都拥有 4GB 的物理地址空间,而只是说能够在 4GB 的范围内寻址。操作系统将会在应用程序运行时,完成 4GB 的虚拟地址和物理内存地址间的转换(页表)。16 位 Windows 下的把代码分成 DATA,CODE 等段的内存模式不同,WIN32 只有一种内存模式,即 FLAT 模式,意思是"平坦"的内存模式,再没有 64K 的段大小限制,所有的 WIN32 的应用程序运行在一个连续、平坦、巨大的 4GB 的空间中。

80x86处理器的工作模式:

实模式,保护模式, 虚拟86模式,实模式,虚拟86模式就是为了兼容老软件,系统应用而做的一种向下兼容的功能,没必要深入了解,保护模式才是目前win32 下CPU的工作模式。
保护模式下最关键的地方就是内存寻址空间增加到4G;采用了优先级机制,分四个级别0-3,0级系统级最高,3级用户级别最低,1 2 是为了兼容alpha设置的,用不到。

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

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


windows的内存管理

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

NULL指针区域: 0x00000000-0x0000FFFF:65535字节

  这个区域的作用是用来帮助程序员发现内存分配失败后未检查就使用的错误。
  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模块,确实是所有设备驱动的代码都再这里。


用户空间: 0x00001000-0x7FFFFFFF:2G-128K

可执行文件和用户自己的dll都加载到这个空间。系统DLL加载到系统内核空间.

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

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

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

从上面看出,并不是所有4G的寻址空间都是可用的,实际可供进程使用的只用2G-128K的空间。


分配粒度和内存页面大小

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的内存区域可供使用呢?

 

函数名修饰符

 

 

程序框架

.386 //使用386指令集(当然你也可以使用.486,.586, 注意.386P/.486P/.586P表明程序可以使用特权级指令)
.MODEL Flat, STDCALL // 内存模式为Flat,参数的传递约定为STDCALL(C和Pascal约定的混合体,参数入栈从右到左,被调者恢复堆栈指针)
.DATA  // 已经初始化的数据
    <Your initialized data> 
 
.DATA? // 未初始化的数据,不会占用可执行文件的大小
    <Your uninitialized data> 
 
.CONST // 常量定
    <Your constants> 
 
.CODE  //
    <label>  // 代码范围
      <Your code> 
        .. 
    end <label> // 代码范围

 

 

消息框例子

 

.386
.model flat, stdcall
option casemap:none // 区分标号的大小写,譬如:start 和 START 是不同的

include \masm32\include\windows.
inc // 常量和结构体的定
include \masm32\include\kernel32.
inc // 我们可能使用到的函数原型
includelib \masm32\lib\kernel32.lib // 会在生成的目标文件中插入链接命令告诉链接器链入什么库

.data
.code
start:
invoke ExitProcess,
0 // INVOKE expression [,arguments], 相对于Call更高层的函数调用方式
end start
 
现在保存例子,取名为msgbox.asm。把 ml.exe 的路径放到 PATH 环境变量中,键入下面一行 进行编译:

ml /c /coff /Cp msgbox.asm (命令行参数大小写是有区别的)

  • /c 是告诉MASM只编译不链接。这主要是考虑到在链接前您可能还有其他工作要做。
  • /coff 告诉MASM产生的目标文件用 coff 格式。MASM 的 coff 格式是COFF(Common Object File Format:通用目标文件格式) 格式的一种变体。在 UNIX 下的 COFF 格式又有不同。
  • /Cp 告诉 MASM 不要更改用户定义的标识符的大小写。若您用的是 hutch 的包含文件的话,在.model 指令下加入 "option casemap:none" 语句,可达到同样的效果。

当您成功的编译了 msgbox.asm 后,编译器会产生 msgbox.obj 目标文件,目标文件和可执行文件只一步之遥,目标文件中包含了以二进制形式存在的指令和数据,比可执行文件相差的只是链接器加入的重定位信息。

链接目标文件:

link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj

  • /SUBSYSTEM:WINDOWS 告诉链接器可执行文件的运行平台
  • /LIBPATH:〈path to import library〉 告诉链接器引入库的路径。

链接器做的工作就是根据引入库往目标文件中加入重定位信息,最后产生可执行文件。

 

函数的原型定义:函数名后加伪指令PROTO,再跟一串由逗号相隔的数据类型链表。

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

下面我们在程序中加入一个对话框。该函数的原型如下:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

  • hWnd 是父窗口的句柄。句柄代表您引用的窗口的一个地址指针。它的值对您编 Windows 程序并不重要(译者注:如果您想成为高手则是必须的),您只要知道它代表一个窗口。当您要对窗口做任何操作时,必须要引用该窗口的指针。
  • lpText 是指向您要显示的文本的指针。指向文本串的指针事实上就是文本串的首地址。
  • lpCaption 是指向您要显示的对话框的标题文本串指针。
  • uType 是显示在对话框窗口上的小图标的类型。
.386 
.model flat,stdcall 
option 
casemap:none 
include \masm32\include\windows.
inc 
include \masm32\include\kernel32.
inc 
includelib \masm32\lib\kernel32.lib 
include \masm32\include\user32.
inc 
includelib \masm32\lib\user32.lib 

.data 
MsgBoxCaption  db 
"Iczelion Tutorial No.2",0 
MsgBoxText       db 
"Win32 Assembly is Great!",0 

.code 
start: 
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK 
invoke ExitProcess, NULL 
end start 
1) NULL 和 MB_OK常量在windows.inc 定义;
2)addr操作符用来把标号MsgBoxText的地址传递给被调用的函数,它只能用在 invoke 语句中,譬如您不能用它来把标号的地址赋给寄存器或变量,如果想这样做则要用offset 操作符:
 
Code

 

 

MASM的优化
posted @ 2009-07-08 11:41  辛勤耕耘  阅读(438)  评论(0)    收藏  举报