在Win32编程中,由于Windows有很多的数据结构和定义,这些都放在include文件中,还有连接时要用到Import库(通俗的讲就是Windows提供的DLL文件中的函数列表,也就是告诉程序到哪里去调用API函数),这些都放在include 和lib目录中。我们在编译时要指定以下的系统环境:
set include=\Masm32v5\Include
set lib=\Masmv5\lib
set path=\Masmv5\Bin
这样编译器就会到正确的路径中去找 include 文件和 lib 文件。你可以自己在 autoexecbat 文件中加上以上语句,为了产生Windows的PE格式的执行文件,在编译和连接中要指定相应的参数:
编译: Ml /c /coff 文件名asm
连接: Link /SUBSYSTEM:WINDOWS OBJ文件名obj 资源文件名res
为了不在每次编译时都要打这么多的参数,我们可以用 nmake 文件来代为执行,nmake 是代码维护程序,他会检查 asm obj exe res 等文件的时间,如果你更新了源程序,他会自动执行编译程序或连接程序产生相应的文件。你可以在文件名为 makefile 的文件中指定使用的编译器和连接程序以及相应的参数,下面是一个 makefile 文件的例子:
NAME = Clock
OBJS = $(NAME)obj
RES = $(NAME)res
$(NAME)exe: $(OBJS) $(RES)
Link /DEBUG /SUBSYSTEM:WINDOWS $(OBJS) $(RES)
$(RES): $(NAME)rc
Rc $(NAME)rc
asmobj:
Ml /c /coff $(NAME)asm
文件告诉 nmake程序,程序名为 clock,产生 clockexe 文件需要 clockobj和 clockres 文件,而产生 clockres 文件需要 clockrc 文件,产生 clockobj 文件要用到 clockasm 文件,至于是否需要执行 ml, link 和 rc,程序会根据文件的时间自动判断。
/电子时钟源代码/
#include<graphicsh>
#include<stdioh>
#include<mathh>
#include<dosh>
#define PI 31415926 /定义常量/
#define UP 0x4800 /上移↑键:修改时间/
#define DOWN 0x5000 /下移↓键:修改时间/
#define ESC 0x11b /ESC键 : 退出系统/
#define TAB 0xf09 /TAB键 : 移动光标/
/函数声明/
int keyhandle(int,int); /键盘按键判断,并调用相关函数处理/
int timeupchange(int); /处理上移按键/
int timedownchange(int); /处理下移按键/
int digithour(double); /将double型的小时数转换成int型/
int digitmin(double); /将double型的分钟数转换成int型/
int digitsec(double); /将double型的秒钟数转换成int型/
void digitclock(int,int,int ); /在指定位置显示时钟或分钟或秒钟数/
void drawcursor(int); /绘制一个光标/
void clearcursor(int);/消除前一个光标/
void clockhandle(); /时钟处理/
double h,m,s; /全局变量:小时,分,秒/
double x,x1,x2,y,y1,y2; /全局变量:坐标值/
struct time t[1];/定义一个time结构类型的数组/
main()
{
int driver, mode=0,i,j;
driver=DETECT; /自动检测显示设备/
initgraph(&driver, &mode, "");/初始化图形系统/
setlinestyle(0,0,3); /设置当前画线宽度和类型:设置三点宽实线/
setbkcolor(0);/用调色板设置当前背景颜色/
setcolor(9); /设置当前画线颜色/
line(82,430,558,430);
line(70,62,70,418);
line(82,50,558,50);
line(570,62,570,418);
line(70,62,570,62);
line(76,56,297,56);
line(340,56,564,56); /画主体框架的边直线/
/arc(int x, int y, int stangle, int endangle, int radius)/
arc(82,62,90,180,12);
arc(558,62,0,90,12);
setlinestyle(0,0,3);
arc(82,418,180,279,12);
setlinestyle(0,0,3);
arc(558,418,270,360,12); /画主体框架的边角弧线/
setcolor(15);
outtextxy(300,53,"CLOCK"); /显示标题/
setcolor(7);
rectangle(342,72,560,360); /画一个矩形,作为时钟的框架/
setwritemode(0); /规定画线的方式。mode=0, 则表示画线时将所画位置的原来信息覆盖/
setcolor(15);
outtextxy(433,75,"CLOCK");/时钟的标题/
setcolor(7);
line(392,310,510,310);
line(392,330,510,330);
arc(392,320,90,270,10);
arc(510,320,270,90,10); /绘制电子动画时钟下的数字时钟的边框架/
/绘制数字时钟的时分秒的分隔符/
setcolor(5);
for(i=431;i<=470;i+=39)
for(j=317;j<=324;j+=7){
setlinestyle(0,0,3);
circle(i,j,1); /以(i, y)为圆心,1为半径画圆/
}
setcolor(15);
line(424,315,424,325); /在运行电子时钟前先画一个光标/
/绘制表示小时的圆点/
for(i=0,m=0,h=0;i<=11;i++,h++){
x=100sin((h60+m)/360PI)+451;
y=200-100cos((h60+m)/360PI);
setlinestyle(0,0,3);
circle(x,y,1);
}
/绘制表示分钟或秒钟的圆点/
for(i=0,m=0;i<=59;m++,i++){
x=100sin(m/30PI)+451;
y=200-100cos(m/30PI);
setlinestyle(0,0,1);
circle(x,y,1);
}
/在电子表的左边打印帮助提示信息/
setcolor(4);
outtextxy(184,125,"HELP");
setcolor(15);
outtextxy(182,125,"HELP");
setcolor(5);
outtextxy(140,185,"TAB : Cursor move");
outtextxy(140,225,"UP : Time ++");
outtextxy(140,265,"DOWN: Time --");
outtextxy(140,305,"ESC : Quit system!");
outtextxy(140,345,"Version : 20");
setcolor(12);
outtextxy(150,400,"Nothing is more important than time!");
clockhandle();/开始调用时钟处理程序/
closegraph(); /关闭图形系统/
return 0; /表示程序正常结束,向 *** 作系统返回一个0值/
}
void clockhandle()
{
int k=0,count;
setcolor(15);
gettime(t);/取得系统时间,保存在time结构类型的数组变量中/
h=t[0]ti_hour;
m=t[0]ti_min;
x=50sin((h60+m)/360PI)+451; /时针的x坐标值/
y=200-50cos((h60+m)/360PI); /时针的y坐标值/
line(451,200,x,y);/在电子表中绘制时针/
x1=80sin(m/30PI)+451; /分针的x坐标值/
y1=200-80cos(m/30PI); /分针的y坐标值/
line(451,200,x1,y1); /在电子表中绘制分针/
digitclock(408,318,digithour(h)); /在数字时钟中,显示当前的小时值/
digitclock(446,318,digitmin(m)); /在数字时钟中,显示当前的分钟值/
setwritemode(1);
/规定画线的方式,如果mode=1,则表示画线时用现在特性的线
与所画之处原有的线进行异或(XOR) *** 作,实际上画出的线是原有线与现在规定
的线进行异或后的结果。因此, 当线的特性不变, 进行两次画线 *** 作相当于没有
画线,即在当前位置处清除了原来的画线/
for(count=2;k!=ESC;){ /开始循环,直至用户按下ESC键结束循环/
setcolor(12);/淡红色/
sound(500);/以指定频率打开PC扬声器,这里频率为500Hz/
delay(700);/发一个频率为500Hz的音调,维持700毫秒/
sound(200);/以指定频率打开PC扬声器,这里频率为200Hz/
delay(300);
/以上两种不同频率的音调,可仿真钟表转动时的嘀哒声/
nosound(); /关闭PC扬声器/
s=t[0]ti_sec;
m=t[0]ti_min;
h=t[0]ti_hour;
x2=98sin(s/30PI)+451; /秒针的x坐标值/
y2=200-98cos(s/30PI); /秒针的y坐标值/
line(451,200,x2,y2);
/绘制秒针/
/利用此循环,延时一秒/
while(t[0]ti_sec==s&&t[0]ti_min==m&&t[0]ti_hour==h)
{ gettime(t);/取得系统时间/
if(bioskey(1)!=0){
k=bioskey(0);
count=keyhandle(k,count);
if(count==5) count=1;
}
}
setcolor(15);
digitclock(485,318,digitsec(s)+1);/数字时钟增加1秒/
setcolor(12); /淡红色/
x2=98sin(s/30PI)+451;
y2=200-98cos(s/30PI);
line(451,200,x2,y2);
/用原来的颜色在原来位置处再绘制秒针,以达到清除当前秒针的目的/
/分钟处理/
if(t[0]ti_min!=m){ /若分钟有变化/
/消除当前分针/
setcolor(15); /白色/
x1=80sin(m/30PI)+451;
y1=200-80cos(m/30PI);
line(451,200,x1,y1);
/绘制新的分针/
m=t[0]ti_min;
digitclock(446,318,digitmin(m)); /在数字时钟中显示新的分钟值/
x1=80sin(m/30PI)+451;
y1=200-80cos(m/30PI);
line(451,200,x1,y1);
}
/小时处理/
if((t[0]ti_hour60+t[0]ti_min)!=(h60+m)){ /若小时数有变化/
/消除当前时针/
setcolor(15); /白色/
x=50sin((h60+m)/360PI)+451;/50:时钟的长度(单位:像素),451:圆心的x坐标值/
y=200-50cos((h60+m)/360PI);
line(451,200,x,y);
/绘制新的时针/
h=t[0]ti_hour;
digitclock(408,318,digithour(h));
x=50sin((h60+m)/360PI)+451;
y=200-50cos((h60+m)/360PI);
line(451,200,x,y);
}
}
}
int keyhandle(int key,int count) /键盘控制 /
{ switch(key)
{case UP: timeupchange(count-1); /因为count的初始值为2,所以此处减1/
break;
case DOWN:timedownchange(count-1); /因为count的初始值为2,所以此处减1/
break;
case TAB:setcolor(15);
clearcursor(count); /清除原来的光标/
drawcursor(count); /显示一个新的光标/
count++;
break;
}
return count;
}
int timeupchange(int count) /处理光标上移的按键/
{
if(count==1){
t[0]ti_hour++;
if(t[0]ti_hour==24) t[0]ti_hour=0;
settime(t); /设置新的系统时间/
}
if(count==2){
t[0]ti_min++;
if(t[0]ti_min==60) t[0]ti_min=0;
settime(t); /设置新的系统时间/
}
if(count==3){
t[0]ti_sec++;
if(t[0]ti_sec==60) t[0]ti_sec=0;
settime(t); /设置新的系统时间/
}
}
int timedownchange(int count) /处理光标下移的按键/
{
if(count==1) {
t[0]ti_hour--;
if(t[0]ti_hour==0) t[0]ti_hour=23;
settime(t);/设置新的系统时间/
}
if(count==2) {
t[0]ti_min--;
if(t[0]ti_min==0) t[0]ti_min=59;
settime(t);/设置新的系统时间/
}
if(count==3) {
t[0]ti_sec--;
if(t[0]ti_sec==0) t[0]ti_sec=59;
settime(t);/设置新的系统时间/
}
}
int digithour(double h)/将double型的小时数转换成int型/
{int i;
for(i=0;i<=23;i++)
{if(h==i) return i;}
}
int digitmin(double m)/将double型的分钟数转换成int型/
{int i;
for(i=0;i<=59;i++)
{if(m==i) return i;}
}
int digitsec(double s) /将double型的秒钟数转换成int型/
{int i;
for(i=0;i<=59;i++)
{if(s==i) return i;}
}
void digitclock(int x,int y,int clock)/在指定位置显示数字时钟:时\分\秒/
{char buffer1[10];
setfillstyle(0,2);
bar(x,y,x+15,328);
if(clock==60) clock=0;
sprintf(buffer1,"%d",clock);
outtextxy(x,y,buffer1);
}
void drawcursor(int count) /根据count的值,画一个光标/
{switch(count)
{
case 1:line(424,315,424,325);break;
case 2:line(465,315,465,325);break;
case 3:line(505,315,505,325);break;
}
}
void clearcursor(int count) /根据count的值,清除前一个光标/
{switch(count)
{
case 2:line(424,315,424,325);break;
case 3:line(465,315,465,325);break;
case 1:line(505,315,505,325);break;
}
}
您正在看的汇编语言是:hello,world!win32汇编小程序。
首先我们看一个“复杂”的Win32汇编程序
程序用来显示一个消息框
--------------------------------------------------
;文件名:3asm
386
model flat ,stdcall
NULL equ 0
MB_OK equ 0
ExitProcess PROTO :Dword
MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:Dword
includelib kernel32lib
includelib user32lib
data
szText db "Hello, world!",0
szCaption db "Win32Asm",0
code
start:
push MB_OK
lea eax,szCaption
push eax
lea eax,szText
push eax
push NULL
call messageboxa
xor eax,eax
push eax
call exitprocess
end start
--------------------------------------------------
编译链接:
分下面两步进行:
ml /c /coff 3asm
link /subsystem:Windows /libpath:d:\masm7\lib 3obj
第一步编译生成3obj文件
/c 表示只编译,不链接
/coff 表示生成COFF格式的目标文件
第二步链接生成3exe文件
/subsystem:windows 表示生成Windows文件
/libpath:d:\masm7\lib 表示引入库的路径为:d:\masm7\lib。
在安装Masm32后,引入库位于Masm32\Lib目录下。
也可设置环境变量Lib的值:在dos提示符下键入Set Lib=d:\masm7\lib,这样“链接”就可简单写成:
link /subsystem:Windows 3obj,试想一下,在程序调试过程中,修改源程序是常用的事啦,每次编译链接都要带/libpath:那该有多烦人呢。当然,我们也可在源程序中直接给出引入库的位置,这样,链接时就方便啦,如下:
includelib d:\masm7\lib\kernel32lib
includelib d:\masm7\lib\user32lib
--------------------------------------------------
执行:在dos提示符下键入3,回车,出现一个消息框,哈哈,真正的Win32程序!
--------------------------------------------------
深入分析:
看一下源程序,有这么两行:call messageboxa\call exitprocess。大家一看都知道,这是子程序调用,但是我们并没写这样的子程序,事实上,这些是API函数。作为函数,我们在调用时可能需要传送给函数一些参数,程序怎么知道传送的参数有哪些,类型是什么呢?就是通过函数原型定义,如下所示:
ExitProcess PROTO :Dword
MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:Dword
可以看出,ExitProcess有一个参数,MessageBoxA有四个参数,这些参数都是Dword类型。
在Win32中,参数的传递都是通过堆栈来完成的。象MessageBoxA这个函数有四个参数,究竟是左边的先压入堆栈还是右边的先入栈呢?model flat,stdcall给出了答案。stdcall 指定参数是从右到左压入堆栈的,且调整堆栈是在子程序返回时完成的。在源程序中不需要用“add sp,值”来保持堆栈平衡。对MessageBox,在API手册中是这样定义的:
int MessageBox(
HWND hWnd, // handle of owner window
LPCTSTR lpText, // address of text in message box
LPCTSTR lpCaption, // address of title of message box
UINT uType &n
您正在看的汇编语言是:hello,world!win32汇编小程序。
bsp; // style of message box
)
;所以会有我们的程序段:
push MB_OK
lea eax,szCaption
push eax
lea eax,szText
push eax
push NULL
call messageboxa
看看上面的程序,不难想到,假如在写程序时,少往堆栈里压入一个数据,那将是一个致命的错误。能不能将这种检查参数个数是否匹配的工作交给计算机来完成呢?这是可以的,INVOKE指令可以帮助我们完成这样的工作。假如你的参数个数不正确,连接器将给出错误提示。所以,极力建议你使用invoke代替call来调用子程序,当然,这不是绝对的。使用invoke上面的指令就可简写成下面的样子,看起来简炼多啦,查错也方便啦!
invoke messageboxa, NULL,addr szText,addr szCaption,MB_OK
另外,像NULL,MB_OK都是一些常量,这样的常量有很多,还有很多的结构,如果在我们的程序中一开始都写这么多的东西,可能一下子就把你吓怕啦,也容易出错,更不便于看程序的主要部分。hutch整理的Windowsinc包含了WIN32编程所需要的常量和结构体的定义,我们可简单的用一个include指令将这些常量和结构的定义插入到我们的文件中:
include d:\masm32\include\Windowsinc
但是Windowsinc中并不包含函数原型的声明,还要从其他的头文件中得到函数原型的声明,比如:messageboxa的原型声明在user32inc文件中,exitprocess在kernel32inc文件中。这些头文件都放在 \masm32\include文件夹下。
还有,要用Windowsinc,必须使用option casemap:none,它的意思是告诉 MASM 要区分符号的大小写,譬如:start和START是不一样的。否则,一个小小的程序,可能会出成百上千的错误呀!
其他的,就不再细说啦,到此,上面的程序可重新修改如下:
-----------------------------------------------------------------
;最终的结果
386 ;表示要用到386指令
model flat,stdcall ;32位程序,要用flat啦!;stadcall,标准调用
option casemap:none ;区别大小写
include Windowsinc ;包括常量及结构定义
include kernel32inc ;函数原型声明
include user32inc
includelib kernel32lib ;用到的引入库
includelib user32lib
data;数据区,定义2个字符串
szText db "Hello, world!",0
szCaption db "Win32Asm",0
code ;代码开始执行处
start:
invoke MessageBox,NULL,addr szText,addr szCaption,MB_OK
;调用MessageBoxAPI函数
invoke ExitProcess,NULL ;程序退出
end start;结束
------------------------------------
编译链接:
ml /c /coff /I d:\masm7\include 3asm ;注意开关符识别大小写
link /subsystem:Windows /libpath:d:\masm7\lib 3obj
/I d:\masm7\include 表示inc文件的位置,也可设置环境变量Set include=d:\masm7\include来简化 *** 作,也可在程序中明确指出inc的位置。
前面讲的都是用两条指令来完成编译链接,实际上用一条指令也可完成,如下:
ml /coff /I d:\masm7\include 3asm /link /subsystem:Windows /libpath:lib
若inc及引入库在源程序中都明确指出其位置,则可简化为:
ml /coff 3asm /link /subsystem:
386
model flat,stdcall
option casemap:none
include C:\masm32\include\windowsinc
include C:\masm32\include\user32inc
includelib C:\masm32\lib\user32lib
include C:\masm32\include\kernel32inc
includelib C:\masm32\lib\kernel32lib
const
szDirectoryName db 'E:\test',0
szFilter db '',0
code
_DeleteFile proc _lpszPath
local @stFindFile:WIN32_FIND_DATA
local @hFindFile
local @szPath[MAX_PATH]:byte
local @szSearch[MAX_PATH]:byte ;
local @szFindFile[MAX_PATH]:byte ;
pushad
invoke lstrcpy,addr @szPath,_lpszPath
@@:
invoke lstrlen,addr @szPath
lea esi,@szPath
add esi,eax
xor eax,eax
mov al,'\'
if byte ptr [esi-1] != al
mov word ptr [esi],ax
endif
invoke lstrcpy,addr @szSearch,addr @szPath
invoke lstrcat,addr @szSearch,addr szFilter
invoke FindFirstFile,addr @szSearch,addr @stFindFile
if eax != INVALID_HANDLE_VALUE
mov @hFindFile,eax
repeat
invoke lstrcpy,addr @szFindFile,addr @szPath
invoke lstrcat,addr @szFindFile,addr @stFindFilecFileName
if @stFindFiledwFileAttributes & FILE_ATTRIBUTE_DIRECTORY
if @stFindFilecFileName != ''
invoke _DeleteFile,addr @szFindFile
invoke RemoveDirectory,addr @szFindFile
endif
else
invoke DeleteFile,addr @szFindFile
endif
invoke FindNextFile,@hFindFile,addr @stFindFile
until (eax ==FALSE)
invoke RemoveDirectory,_lpszPath
invoke FindClose,@hFindFile
endif
popad
ret
_DeleteFile endp
_DeleteDirectory proc _lpszDirectoryName
invoke _DeleteFile,_lpszDirectoryName
invoke RemoveDirectory,_lpszDirectoryName
ret
_DeleteDirectory endp
start:
invoke _DeleteDirectory,addr szDirectoryName
end start
理论:
Win32有一些供程序员使用的API,它们提供相当于调试器的功能 他们被称作Win32调试API(或原语)利用这些API,我们可以:
加载一个程序或捆绑到一个正在运行的程序上以供调试
获得被调试的程序的低层信息,例如进程ID,进入地址,映像基址等
当发生与调试有关的事件时被通知,例如进程/线程的开始/结束, DLL的加载/释放等
修改被调试的进程或线程
简而言之,我们可以用这些API写一个简单的调试器由于这个题目有些过大,我把它分为几部分,而本教程就是它的第一部分在本教程中,我将讲解一些基本概念及Win32调试API的大致框架
使用Win32调试API的步骤如下:
创建一个进程或捆绑到一个运行中的进程上 这是使用Win32调试API的第一步由于我们的程序要扮演调试器的角色,我们要找一个供调试的程序一个被调试的程序被称为debuggee可以通过以下两种方式获得debuggee:
通过CreateProcess创建debuggee进程为了创建被调试的进程,必须指定DEBUG_PROCESS标志这一标志告诉Windows我们要调试该进程 当debuggee中发生重要的与调试有关的事件(调试事件)时,Windows 会向我们的程序发送通知debuggee会立即挂起以等待我们的程序准备好如果debuggee还创建了子进程,Windows还会为每个子进程中的调试事件向我们的程序发送通知这一特性通常是不必要的我们可以通过指定DEBUG_ONLY_THIS_PROCESS与 DEBUG_PROCESS的组合标志来禁止它
我们也可以用 DebugActiveProcess标志捆绑到一个运行中的进程上
等待调试事件 在获得了一个debuggee进程后,debuggee的主线程被挂起,这种状况将持续到我们的程序调用WaitForDebugEvent为止这个函数和其他的WaitForXXX函数相似,比如说,它阻塞调用线程直到等待的事件发生对这个函数来说, 它等待由Windows发送的调试事件下面是它的定义:
WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD
lpDebugEvent is the address of a DEBUG_EVENT这个结构将被填入关于debuggee中发生的调试事件的信息
dwMilliseconds 该函数等待调试事件的时间,以毫秒为单位如果这段时间没有调试事件发生, WaitForDebugEvent返回调用者另一方面,如果将该参数指定为 INFINITE 常数,函数将一直等待直到调试事件发生
现在我们看一下DEBUG_EVENT 结构
DEBUG_EVENT STRUCT
dwDebugEventCode dd
dwProcessId dd
dwThreadId dd
u DEBUGSTRUCT <>
DEBUG_EVENT ENDS
dwDebugEventCode 该值指定了等待发生的调试事件的类型因为有很多种类型的事件发生,我们的程序要检查该值,知道要发生事件的类型并做出响应 该值可能的取值如下:
取值 含义
CREATE_PROCESS_DEBUG_EVENT 进程被创建当debuggee进程刚被创建(还未运行) 或我们的程序刚以DebugActiveProcess被捆绑到一个运行中的进程时事件发生 这是我们的程序应该获得的第一个事件
EXIT_PROCESS_DEBUG_EVENT 进程退出
CREATE_THEAD_DEBUG_EVENT 当一个新线程在deuggee进程中创建或我们的程序首次捆绑到运行中的进程时事件发生要注意的是当debugge的主线程被创建时不会收到该通知
EXIT_THREAD_DEBUG_EVENT debuggee中的线程退出时事件发生debugee的主线程退出时不会收到该通知我们可以认为debuggee的主线程与debugge进程是同义词 因此, 当我们的程序看到CREATE_PROCESS_DEBUG_EVENT标志时,对主线程来说,就是CREATE_THREAD_DEBUG_EVENT标志
LOAD_DLL_DEBUG_EVENT debuggee装入一个DLL当PE装载器第一次分解指向DLL的链接时,我们将收到这一事件 (当调用CreateProcess装入 debuggee时)并且当debuggee调用LoadLibrary时也会发生
UNLOAD_DLL_DEBUG_EVENT 一个DLL从debuggee中卸载时事件发生
EXCEPTION_DEBUG_EVENT 在debuggee中发生异常时事件发生 注意: 该事件仅在debuggee开始它的第一条指令之前发生一次异常实际上是一个调试中断(int 3h)如果想恢复debuggee事,以 DBG_CONTINUE 标志调用ContinueDebugEvent 函数 不要使用DBG_EXCEPTION_NOT_HANDLED 标志否则debuggee会在NT下拒绝运行(Win98下运行得很好)
OUTPUT_DEBUG_STRING_EVENT 当debuggee调用DebugOutputString函数向我们的程序发送消息字符串时该事件发生
RIP_EVENT 系统调试发生错误
dwProcessId 和dwThreadId发生调试事件的进程和线程Id我们可以用这些值作为我们感兴趣的进程或线程的标志符记住如果我们使用CreateProcess来装载debuggee,我们仍可在PROCESS_INFO结构中获得debuggee的进程和线程我们可以用这些值来区别调试事件是发生在debuggee中还是它的子进程中(当没有指定 DEBUG_ONLY_THIS_PROCESS 标志时)
u 是一个联合,包含了调试事件的更多信息根据上面dwDebugEventCode的不同,它可以是以下结构:
dwDebugEventCode u的解释
CREATE_PROCESS_DEBUG_EVENT 名为CreateProcessInfo的CREATE_PROCESS_DEBUG_INFO结构
EXIT_PROCESS_DEBUG_EVENT 名为ExitProcess的EXIT_PROCESS_DEBUG_INFO结构
CREATE_THREAD_DEBUG_EVENT 名为CreateThread的CREATE_THREAD_DEBUG_INFO结构
EXIT_THREAD_DEBUG_EVENT 名为ExitThread的EXIT_THREAD_DEBUG_EVENT 结构
LOAD_DLL_DEBUG_EVENT 名为LoadDll的LOAD_DLL_DEBUG_INFO 结构
UNLOAD_DLL_DEBUG_EVENT 名为UnloadDll的UNLOAD_DLL_DEBUG_INFO结构
EXCEPTION_DEBUG_EVENT 名为Exception的EXCEPTION_DEBUG_INFO结构
OUTPUT_DEBUG_STRING_EVENT 名为DebugString的OUTPUT_DEBUG_STRING_INFO 结构
RIP_EVENT 名为RipInfo的RIP_INFO 结构
我不会在这一个教程里讲所有这些结构的细节,这里只详细讲一下CREATE_PROCESS_DEBUG_INFO 结构
假设我们的程序调用了WaitForDebugEvent函数并返回,我们要做的第一件事就是检查dwDebugEventCode中的值来看debuggee进程中发生了那种类型的调试事件比如说,如果dwDebugEventCode的值为 CREATE_PROCESS_DEBUG_EVENT,就可认为u的成员为CreateProcessInfo 并用uCreateProcessInfo来访问
在我们的程序中做对调试事件的响应 当WaitForDebugEvent 返回时,这意味着在debuggee进程中发生了调试事件或者发生了超时所以我们的程序要检查dwDebugEventCode 来作出适当的反应这里有些象处理Windows消息:由用户来选择和忽略消息
继续运行debuggee 当调试事件发生时, Windows挂起了debuggee,所以当我们处理完调试事件,还要让debuggee继续运行调用ContinueDebugEvent 函数来完成这一过程
ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD
该函数恢复由于调试事件而挂起的线程
dwProcessId和dwThreadId是要恢复的线程的进程ID和线程ID,通常这两个值从 DEBUG_EVENT结构的dwProcessId 和dwThreadId成员获得
dwContinueStatus显示了如何继续报告调试事件的线程可能的取值有两个: DBG_CONTINUE 和DBG_EXCEPTION_NOT_HANDLED 对大多数调试事件,这两个值都一样:恢复线程唯一的例外是EXCEPTION_DEBUG_EVENT,如果线程报告发生了一个异常调试事件,这意味着在debuggee的线程中发生了一个异常如果指定了DBG_CONTINUE,线程将忽略它自己的异常处理部分并继续执行在这种情况下,我们的程序必须在以DBG_CONTINUE恢复线程之前检查并处理异常,否则异常将生生不息地不断发生如果我们指定了 DBG_EXCEPTION_NOT_HANDLED值,就是告诉Windows我们的程序并不处理异常:Windows将使用debuggee的默认异常处理函数来处理异常
总而言之,如果我们的程序没有考虑异常,而调试事件又指向debuggee进程中的一个异常的话,就应调用含DBG_CONTINUE标志的ContinueDebugEvent函数否则,我们的程序就必须以DBG_EXCEPTION_NOT_HANDLED调用 ContinueDebugEvent但在下面这种情况下必须使用DBG_CONTINUE标志:第一个在ExceptionCode成员中有值EXCEPTION_BREAKPOINT的 EXCEPTION_DEBUG_EVENT事件当debuggee开始执行它的第一条指令时,我们的函数将接受到异常调试事
件它事实上是一个调试中断(int 3h)如果我们以DBG_EXCEPTION_NOT_HANDLED调用ContinueDebugEvent 来响应调试事件, Windows NT会拒绝执行debuggee(因为它没有异常处理)所以在这种情况下,要用DBG_CONTINUE标志告诉Windows我们希望该线程继续执行
继续上面的步骤循环直到debuggee进程退出 我们的程序必须在一个很象消息循环的无限循环中直到debuggee结束该循环大体如下:
while TRUE
invoke WaitForDebugEvent, addr DebugEvent, INFINITE
break if DebugEventdwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
<调试事件处理>
invoke ContinueDebugEvent, DebugEventdwProcessId, DebugEventdwThreadId, DBG_EXCEPTION_NOT_HANDLED
endw
就是说,当开始调试程序时,我们的程序不能和debuggee分开直到它结束
我们再来总结一下这些步骤:
创建一个进程或捆绑我们的程序到运行中的进程上
等待调试事件
响应调试事件
继续执行debuggee
继续这一无尽循环直到debuggee进程结束
例子:
这个例子调试一个win32程序并显示诸如进程句柄,进程Id,映象基址等
386
model flat,stdcall
option casemap:none
include \masm32\include\windowsinc
include \masm32\include\kernel32inc
include \masm32\include\comdlg32inc
include \masm32\include\user32inc
includelib \masm32\lib\kernel32lib
includelib \masm32\lib\comdlg32lib
includelib \masm32\lib\user32lib
data data
AppName db "Win32 Debug Example no1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"exe",0
db "All Files",0,"",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
db "Process Handle: %lx",0Dh,0Ah
db "Thread Handle: %lx",0Dh,0Ah
db "Image Base: %lx",0Dh,0Ah
db "Start Address: %lx",0
data
buffer db 512 dup()
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
code
start:
mov ofnlStructSize,sizeof ofn
mov ofnlpstrFilter, offset FilterString
mov ofnlpstrFile, offset buffer
mov ofnnMaxFile,512
mov ofnFlags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
if DBEventdwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
break elseif DBEventdwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEventuCreateProcessInfohFile, DBEventuCreateProcessInfohProcess, DBEventuCreateProcessInfohThread, DBEventuCreateProcessInfolpBaseOfImage, DBEventuCreateProcessInfolpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
elseif DBEventdwDebugEventCode==EXCEPTION_DEBUG_EVENT
if DBEventuExceptionpExceptionRecordExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEventdwProcessId, DBEventdwThreadId, DBG_CONTINUE
continue
endif
elseif DBEventdwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
elseif DBEventdwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
endif
invoke ContinueDebugEvent, DBEventdwProcessId, DBEventdwThreadId, DBG_EXCEPTION_NOT_HANDLED
endw
invoke CloseHandle,pihProcess
invoke CloseHandle,pihThread
endif
invoke ExitProcess, 0
end start
分析:
程序首先填充OPENFILENAME结构,调用GetOpenFileName让用户选择要调试的程序
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
当接收用户选择后,调用CreateProcess装载程序并调用GetStartupInfo以默认值填充STARTUPINFO结构注意我们将DEBUG_PROCESS标志与DEBUG_ONLY_THIS_PROCESS标志组合来仅调试这个程序,不包括子进程
while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
在debuggee被装入后,我们调用WaitForDebugEvent进入无尽的调试循环,WaitForDebugEvent在debuggee中发生调试事件时返回,因为我们指定了INFINITE作为第二个参数当调试事件发生时, WaitForDebugEvent 返回并填充DBEvent结构
if DBEventdwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
break
我们要先检查dwDebugEventCode的值, 如果是EXIT_PROCESS_DEBUG_EVENT,用一个消息框显示"The debuggee exits" 并退出调试循环
elseif DBEventdwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEventuCreateProcessInfohFile, DBEventuCreateProcessInfohProcess, DBEventuCreateProcessInfohThread, DBEventuCreateProcessInfolpBaseOfImage, DBEventuCreateProcessInfolpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
如果dwDebugEventCode 的值为CREATE_PROCESS_DEBUG_EVENT,我们就在消息框中显示一些感兴趣的底层信息这些信息从uCreateProcessInfo获得 CreateProcessInfo是一个CREATE_PROCESS_DEBUG_INFO类型的结构体你可以查阅Win32 API获得它的更多信息e
elseif DBEventdwDebugEventCode==EXCEPTION_DEBUG_EVENT
if DBEventuExceptionpExceptionRecordExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEventdwProcessId, DBEventdwThreadId, DBG_CONTINUE
continue
endif
如果dwDebugEventCode 的值为EXCEPTION_DEBUG_EVENT,我们就要更进一步检查异常类型它是一大堆的结构嵌套,但我们可以从ExceptionCode成员获得异常类型如果ExceptionCode的值为 EXCEPTION_BREAKPOINT并且是第一次发生(或者我们已知道deuggee中没有int 3h指令),我们可以安全地假定在debuggee要执行第一条指令时发生这一异常在我们完成这些处理后,就可以用 DBG_CONTINUE调用ContinueDebugEvent来继续执行debuggee接着我们继续等待下一个调试事件的发生
elseif DBEventdwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
elseif DBEventdwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
endif
如果dwDebugEventCode 的值为CREATE_THREAD_DEBUG_EVENT或EXIT_THREAD_DEBUG_EVENT, 我们的程序显示一个消息框
invoke ContinueDebugEvent, DBEventdwProcessId, DBEventdwThreadId, DBG_EXCEPTION_NOT_HANDLED
endw
除了上面讨论过的 EXCEPTION_DEBUG_EVENT,用DBG_EXCEPTION_NOT_HANDLED标志调用ContinueDebugEvent函数恢复debuggee的执行
invoke CloseHandle,pihProcess
invoke CloseHandle,pihThread
当debuggee结束时,我们就跳出了调试循环,这时要关闭 debuggee的线程和进程句柄关闭这些句柄并不意味着要关闭这些进程和线程只是说不再用这些句柄罢了
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)