日历
| |||||||||
| 日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
| 1 | 2 | 3 | 4 | ||||||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 | |||
| 12 | 13 | 14 | 15 | 16 | 17 | 18 | |||
| 19 | 20 | 21 | 22 | 23 | 24 | 25 | |||
| 26 | 27 | 28 | 29 | 30 | 31 | ||||
存档
搜索标题
最新评论
统计信息
- 访问量: 125
- 日志数: 6
- 建立时间: 2008-08-28
- 更新时间: 2008-09-11
我的最新日志
-
loadrunner - winsock 函数
2008-9-11
lrs_accept_connection 接受侦听套接字连接
lrs_close_socket 关闭打开的套接字
lrs_create_socket 初始化套接字
lrs_disable_socket 禁用套接字操作
lrs_exclude_socket 重播期间排除套接字
lrs_get_socket_attrib 获取套接字属性
lrs_get_socket_handler 获取指定套接字的套接字处理程序
lrs_length_receive 接收来自指定长度的缓冲区的数据
lrs_receive 接收来自套接字的数据
lrs_receive_ex 接收来自数据报或流套接字的数据(具有特定长度)
lrs_send 将数据发送到数据报上或流套接字中
lrs_set_receive_option 设置套接字接收选项
lrs_set_socket_handler 设置特定套接字的套接字处理程序
lrs_set_socket_options 设置套接字选项
缓冲区函数lrs_free_buffer 释放分配给缓冲区的内存
lrs_get_buffer_by_name 从数据文件中获取缓冲区及其大小
lrs_get_last_received_buffer 获取套接字上接收到的最后的缓冲区及其大小
lrs_get_last_received_buffer_size 获取套接字上接收到的最后一个缓冲区的大小
lrs_get_received_buffer 获取最后接收到的缓冲区或其一部分
lrs_get_static_buffer 获取静态缓冲区或其一部分
lrs_get_user_buffer 获取套接字的用户数据的内容
lrs_get_user_buffer_size 获取套接字的用户数据的大小
lrs_set_send_buffer 指定要在套接字上发送的缓冲区
环境函数lrs_cleanup 终止 Windows 套接字 DLL 的使用
lrs_startup 初始化 Windows 套接字 DLL
关联语句函数lrs_save_param 将静态或接收到的缓冲区(或缓冲区部分)保存到参数中
lrs_save_param_ex 将用户、静态或接收到的缓冲区(或缓冲区部分)保存到参数中
lrs_save_searched_string 在静态或接收到的缓冲区中搜索出现的字符串,将出现字符串的缓冲区部分保存到参数中
转换函数lrs_ascii_to_ebcdic 将缓冲区数据从 ASCII 格式转换成 EBCDIC 格式
lrs_decimal_to_hex_string 将十进制整数转换为十六进制字符串
lrs_ebcdic_to_ascii 将缓冲区数据从 EBCDIC 格式转换成ASCII 格式
lrs_hex_string_to_int 将十六进制字符串转换为整数
超时函数lrs_set_accept_timeout 为接受套接字设置超时
lrs_set_connect_timeout 为连接到套接字设置超时
lrs_set_recv_timeout 为接收套接字上的初始预期数据设置超时
lrs_set_recv_timeout 为建立连接后接收套接字上的预期数据设置超时
lrs_set_send_timeout 为发送套接字数据设置超时
录制会话之后,通过 VuGen 的内置编辑器可以查看录制的代码。您可以在脚本中滚动,查看应用程序生成的函数,并检查传输的数据。在主窗口中查看脚本时,可以看到VuGen 录制活动的顺序。在典型的会话期间,将录制下列函数顺序:
lrs_startup 初始化 WinSock DLL
lrs_create_socket 初始化套接字
lrs_send 在数据报上或者向流套接字发送数据
lrs_receive 接收来自数据报或流套接字的数据
lrs_disable_socket 禁用套接字操作
lrs_close_socket 关闭打开的套接字
lrs_cleanup 终止 WinSock DLL 的使用VuGen 在 Windows 上使用 Windows 套接字协议支持应用程序的录制和重播;而在UNIX 平台上仅支持重播。
-
深入浅出HOOKS
2008-9-11
hook是WINDOWS提供的一种消息处理机制,它使得程序员可以使用子过程来监视系统消息,并在消息到达目标过程前得到处理。
下面将介绍WINNDOWS HOOKS并且说明如何在WINDOWS 程序中使用它。
使用HOOK 将会降低系统效率,因为它增加了系统处量消息的工作量。建议在必要时才使用HOOK,并在消息处理完成后立即移去该HOOK。
HOOK链
WINDOWS提供了几种不同类型的HOOKS;不同的HOOK可以处理不同的消息。例如,WH_MOUSE HOOK用来监视鼠标消息。
WINDOWS为这几种HOOKS维护着各自的HOOK链。HOOK链是一个由应用程序定义的回调函数队列,当某种类型的消息发生时,WINDOWS向此种类型的HOOK链的第一个函数发送该消息,在第一函数处理完该消息后由该函数向链表中的下一个函数传递消息,依次向下。如果链中某个函数没有向下传送该消息,那么链表中后面的函数将得不到此消息。(对于某些类型的HOOK,不管HOOK链中的函数是否向下传递消息,与此类型HOOK联系的所有HOOK函数都会收到系统发送的消息)
===========================HOOK过程========================
为了拦截特定的消息,你可以使用SetWindowsHookEx函数在该类型的HOOK链中安装你自己的HOOK函数。该函数语法如下:
public function MyHook(nCode,wParam,iParam) as long
‘加入代码
end function
其中MyHook可以随便命名,其它不能变。该函数必须放在模块段。nCode指定HOOK类型。wParam,iParam的取值随nCode不同而不同,它代表了某种类型的HOOK的某个特定的动作。
SetWindowsHookEx总是将你的HOOK函数放置在HOOK链的顶端。你可以使用CallNextHookEx函数将系统消息传递给HOOK链中的下一个函数。
[注释]对于某些类型的HOOK,系统将向该类的所有HOOK函数发送消息,这时,HOOK函数中的CallNextHookEx语句将被忽略。
全局HOOK函数可以拦截系统中所有线程的某个特定的消息(此时该HOOK函数必须放置在DLL中),局部HOOK函数可以拦截指定线程的某特定消息(此时该HOOK函数可以放置在DLL中,也可以放置在应用程序的模块段)。
[注释] 建议只在调试时使用全局HOOK函数。全局HOOK函数将降低系统效率,并且会同其它使用该类HOOK的应用程序产生冲突。
========================HOOK类型=================================
WH_CALLWNDPROC 和 WH_CALLWNDPROCRET HOOK
WH_CALLWNDPROC 和WH_CALLWNDPROCRET HOOK可以监视SendMessage发送的消息。系统在向窗体过程发送消息前,将调用WH_CALLWNDPROC;在窗体过程处理完该消息后系统将调用WH_CALLWNDPROCRET。
WH_CALLWNDPROCRET HOOK会向HOOK过程传送一个CWPRETSTRUCT结构的地址。该结构包含了窗体过程处理系统消息后的一些信息。
WH_CBT Hook
系统在激活,创建,消毁,最小化,最大化,移动,改变窗体前;在完成一条系统命令前;在从系统消息队列中移去鼠标或键盘事件前;在设置输入焦点前,或同步系统消息队列前,将调用WH_CBT HOOK。你可以在你的HOOK 过程拦截该类HOOK,并返回一个值,告诉系统,是否继续执行上面的操作。
WH_DEBUG HOOK
系统在调用与某种HOOK类型联系的HOOK过程前,将调用WH_DEBUG ,应用程序可以使用该HOOK决定是否让系统执行某种类型的HOOK。
WH_FOREGROUNDIDLE Hook
系统在空闲时调用该HOOK,在后台执行优先权较低的应用程序。
WH_GETMESSAGE Hook
WH_GETMESSAGE Hook使应用程序可以拦截GetMessage 或 PeekMessage的消息。应用程序使用WH_GETMESSAGE HOOK监视鼠标、键盘输入和发送到队列中的其它消息。
WH_JOURNALRECORD Hook
WH_JOURNALRECORD Hook使应用程序可以监视输入事件。典型地,应用程序使用该HOOK记录鼠标、键盘输入事件以供以后回放。该HOOK是全局HOOK,并且不能在指定线程中使用。
WH_JOURNALPLAYBACK Hook
` WH_JOURNALPLAYBACK Hook使应用程序可以向系统消息队列中插入消息。该HOOK可以回放以前由WH_JOURNALRECORD HOOK录制的鼠标、键盘输入事件。在WH_JOURNALPLAYBACK Hook安装到系统时,鼠标、键盘输入事件将被屏蔽。该HOOK同样是一个全局HOOK,不能在指定线程中使用。
WH_JOURNALPLAYBACK Hook返回一个时间暂停值,它告诉系统,在处理当前回放的消息时,系统等待百分之几秒。这使得此HOOK可以控制在回放时的时间事件。
WH_KEYBOARD Hook
WH_KEYBOARD Hook使应用程序可以监视由GetMessage和PeekMessage返回的WM_KEYDOWN 及WM_KEYUP消息。应用程序使用该HOOK监视发送到消息队列中的键盘输入。
WH_MOUSE Hook
WH_MOUSE Hook 使应用程序可以监视由GetMessage和PeekMessage返回的消息。应用程序使用该HOOK监视发送到消息队列中的鼠标输入。
WH_MSGFILTER and WH_SYSMSGFILTER Hooks
WH_MSGFILTER 和WH_SYSMSGFILTER Hooks使应用程序可以监视菜单、滚动条、消息框、对话框,当用户使用ALT+TAB或ALT+ESC来切换窗体时,该HOOK也可以拦截到消息。WH_MSGFILTER仅在应用程序内部监视菜单、滚动条、消息框、对话框,而WH_SYSMSGFILTER则可以在系统内监视所有应用程序的这些事件。
WH_SHELL Hook
一个SHELL程序可以使用WH_SHELL Hook来接收重要的信息。当一个SHELL程序被激活前或当前窗体被创建、消毁时,系统会调用WH_SHELL Hook过程。
=======================使用HOOK============================
安装、消毁HOOK过程
监视系统事件
安装、消毁HOOK过程
使用SetWindowsHookEx函数,指定一个HOOK类型,自己的HOOK过程是全局还是局部HOOK,同时给出HOOK过程的进入点,就可以轻松的安装你自己的HOOK过程。
为了安装一个全局HOOK过程,必须在应用程序外建立一个DLL,并将该HOOK函数封装到其中,应用程序在安装全局HOOK过程时必须先得到该DLL模块的句柄。将DLL名传递给LoadLibrary 函数,就会得到该DLL模块的句柄;得到该句柄 后,使用GetProcAddress函数可以得到HOOK过程的地址。最后,使用SetWindowsHookEx将HOOK过程的首址嵌入相应的HOOK链中,SetWindowsHookEx传递一个模块句柄,它为HOOK过程的进入点,线程标识符置为0,指出:该HOOK过程同系统中的所有线程关联。
以下是C写的例程,大家可以方便的转换为VB程序。
HOOKPROC hkprcSysMsg;
static HINSTANCE hinstDLL;
static HHOOK hhookSysMsg;
.
.
.
hinstDLL = LoadLibrary((LPCTSTR) "c:\\windows\\sysmsg.dll");
hkprcSysMsg = (HOOKPROC)GetProcAddress(hinstDLL, "SysMessageProc");
hhookSysMsg = SetWindowsHookEx(WH_SYSMSGFILTER,
hkprcSysMsg, hinstDLL, 0);
Hook简介
2000-03-18· -·cww
Hook这个东西有时令人又爱又怕,Hook是用来拦截系统某些信息之用,例如说,我们想 让系统不管在什麽地方只要按个Ctl-B便执行NotePad,或许您会使用Form的KeyPreview,设定为True,但在其他Process中按Ctl-B呢?那就没有用,这时就得设一个Keyboard Hook来拦截所有Key in的键;再如:MouseMove的Event只在该Form或Control上有效,如 果希望在Form的外面也能得知Mouse Move的信息,那只好使用Mouse Hook来栏截Mouse 的信息。再如:您想记录方才使用者的所有键盘动作或Mosue动作,以便录巨集,那就使用JournalRecordHook,如果想停止所有Mosue键盘的动作,而放(执行)巨集,那就使用JournalPlayBack Hook;Hook呢,可以是整个系统为范围(Remote Hook),即其他 Process的动作您也可以拦截,也可以是LocalHook,它的拦截范围只有Process本身。Remote Hook的Hook Function要在.Dll之中,Local Hook则在.Bas中。
在VB如何设定Hook呢?使用SetWindowsHookEx()
Declare Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA"(ByVal idHook As Long, ByVal lpfn As Long, ByVal hmod As Long, ByVal dwThreadId As Long) As Long
idHook代表是何种Hook,有以下几种
Public Const WH_CALLWNDPROC = 4
Public Const WH_CALLWNDPROCRET = 12
Public Const WH_CBT = 5
Public Const WH_DEBUG = 9
Public Const WH_FOREGROUNDIDLE = 11
Public Const WH_GETMESSAGE = 3
Public Const WH_HARDWARE = 8
Public Const WH_JOURNALPLAYBACK = 1
Public Const WH_JOURNALRECORD = 0
Public Const WH_KEYBOARD = 2
Public Const WH_MOUSE = 7
Public Const WH_MSGFILTER = (-1)
Public Const WH_SHELL = 10
Public Const WH_SYSMSGFILTER = 6
pfn代表Hook Function所在的Address,这是一个CallBack Fucnction,当挂上某个Hook时,我们便得定义一个Function来当作某个信息产生时,来处理它的Function,这个Hook Function有一定的参数格式
Private Function HookFunc(ByVal nCode As Long,ByVal wParam As Long, ByVal lParam As Long ) As Long
nCode 代表是什麽请况之下所产生的Hook,随Hook的不同而有不同组的可能值wParam lParam 传回值则随Hook的种类和nCode的值之不同而不同。
因这个参数是一个 Function的Address所以我们固定将Hook Function放在.Bas中,并以AddressOf HookFunc传入。至于Hook Function的名称我们可以任意给定,不一定叫HookFunchmod 代表.DLL的hInstance,如果是Local Hook,该值可以是Null(VB中可传0进去),而如果是Remote Hook,则可以使用GetModuleHandle(".dll名称")来传入。
dwThreadId 代表执行这个Hook的ThreadId,如果不设定是那个Thread来做,则传0(所以 一般来说,Remote Hook传0进去),而VB的Local Hook一般可传App.ThreadId进去值回值 如果SetWindowsHookEx()成功,它会传回一个值,代表目前的Hook的Handle, 这个值要记录下来。
因为A程序可以有一个System Hook(Remote Hook),如KeyBoard Hook,而B程序也来设一 个Remote的KeyBoard Hook,那麽到底KeyBoard的信息谁所拦截?答案是,最後的那一个 所拦截,也就是说A先做keyboard Hook,而後B才做,那信息被B拦截,那A呢?就看B的 Hook Function如何做。如果B想让A的Hook Function也得这个信息,那B就得呼叫 CallNextHookEx()将这信息Pass给A,於是产生Hook的一个连线。如果B中不想Pass这信息给A,那就不要呼叫CallNextHookEx()。
Declare Function CallNextHookEx Lib "user32" Alias "CallNextHookEx" _
(ByVal hHook As Long, _
ByVal ncode As Long, _
ByVal wParam As Long, _
lParam As Any) As Long
hHook值是SetWindowsHookEx()的传回值,nCode, wParam, lParam则是Hook Procedure 中的三个参数。
最後是将这Hook去除掉,请呼叫UnHookWindowHookEx()
Declare Function UnhookWindowsHookEx Lib "user32" Alias "UnhookWindowsHookEx" _
(ByVal hHook As Long) As Long
hHook便是SetWindowsHookEx()的传回值。此时,以上例来说,B程序结束Hook,则换A可 以直接拦截信息。
KeyBoard Hook的范例
Hook Function的三个参数
nCode wParam lParam 传回值
HC_ACTION或HC_NOREMOVE 表按键Virtual Key 与WM_KEYDOWN同 若信息要被处理传0 反之传1
Public hHook as Long
Public Sub UnHookKBD()
If hnexthookproc <> 0 Then
UnhookWindowsHookEx hHook
hHook = 0
End If
End Sub
Public Function EnableKBDHook()
If hHook <> 0 Then Exit Function
hhook = SetWindowsHookEx(WH_KEYBOARD, AddressOf MyKBHFunc, App.hInstance, App.ThreadId)
End Function
Public Function MyKBHFunc(ByVal iCode As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
MyKBHfunc = 0 '表示要处理这个信息
If wParam = vbKeySnapshot Then '侦测 有没有按到PrintScreen键
MyKBHFunc = 1 '在这个Hook便吃掉这个信息
End If
Call CallNextHookEx(hHook, iCode, wParam, lParam) '传给下一个Hook
End Function
至於其他的 Hook的详细资料与nCode,wParam, lParam的意义,请查Win32 Help
如何获得密码窗口的内容------揭开Hook的面纱
Pan Ying(zero world)
现在的不少程序都有取得密码窗口的内容的功能,你有没有想过是如何做到这一切的呢?我有一个同学就问过我这样一个问题,当时我也回答不出来,现在我研究了视窗的Hook函数之后总算有了一定的了解。下面就有我来揭开Hook函数的神秘面纱。
先来介绍Hook的含义:
Hook是指在程序正常运行中接受信息之前预先启动的函数,用来检查和修改传给该程序的信息。举例来说,当你在一个窗口上点击鼠标以后,首先接收信息的是相应的Hook函数,然后才传到相应的应用程序。
如何定义Hook函数:
SetWindowsHookEx:
用来装入新的Hook函数,其参数列表为下:
int idHook:装入Hook类型,有WH_MOUSE 等。
HOOKPROC lpfn:要装入的Hook函数地址。
HINSTANCE hMod:函数所在的进程,如果为全局Hook函数该函数必须在Dll中。
DWORD dwThreadId:装入哪个进程,如果为全局,则为0。
返回相应Hook标识。
HookProc:
您自定义的Hook函数,应具有的参数如下:
int nCode:传入的信息类型。
WPARAM wParam:短整型参数。
LPARAM lParam:长整型参数。
要在函数中调用CallNextHookEx把信息传给下一个Hook函数。
CallNextHookEx:
用于将信息传给下一个Hook函数,需要将得到的Hook标识和相应参数传入。
UnhookWindowsHookEx:
用于将装入的Hook函数从Hook链表中剔除,应传入得到的Hook标识。
下面我们来看一个例子:
例子原码的下载:HookTest.zip 本程序在Window98和Delphi5下通过。
一、调用LoadLibrary和GetProcAddress取得函数地址。
hinstDLL := LoadLibrary(LPCTSTR( 'hooktest.dll'));
@hkprcSysMsg:=GetProcAddress(hinstDLL, 'MouseProc');
二、用SetWindowsHookEx将得到的函数装入。
hhk:= SetWindowsHookEx(WH_MOUSE,@hkprcSysMsg,hinstDLL, 0);
三、Windows会将函数装入到每一个进程中。
四、每当鼠标点击,自定义的Hook函数会将点击的窗口标题传给程序的标题。
if (nCode=HC_ACTION)and(WPARAM=WM_LBUTTONDOWN)
then
begin
MyMouse:=CPointer(lPARAM);
MyHandle2:=MyMouse^.hwnd;
GetMem(MyString,GetWindowTextlength(myhandle2)+1);
GetWindowText(MyHandle2,MyString,GetWindowTextlength(myhandle2)+1);
TempHandle:=myhandle2;
while (TempHandle<>0) do
begin
myHandle2:=TempHandle;
TempHandle:=GetParent(TempHandle);
end;
if (myhandle2<>0)
then
SetWindowText(myhandle2,MyString);
FreeMem(MyString);
end
就这样完成了得到密码窗口内容的功能。
关于 HOOK
[ 作者: 陆麟 添加时间: 2001-6-2 0:04:44 ]
来源:lu0.126.com
大家讨论HOOK太多了.在网络中,概凡谈论到进程控制,十有八九最后会得到一句话:写HOOK.
嘿嘿,知道HOOK运作机理的有几个呢?下面,本人就MSDN文档中没有写到的一点东西稍微讲几句.
以下讲述乃针对全局HOOK而发.
1.设置HOOK过程时返回的前一HOOK地址必须被保存到一个全局共享的内存地址段中.这个共享段的地址不是什么本进程的全局变量,而是所有进程都可以看见的变量.因为,进程级变量进在本进程内可见.当其他进程加载HOOK DLL时,HOOK DLL里的所有变量都会被RESET.这也就是说:
HHOOK hk;
//set and get HHOOK here
return hk();
这样的描述是不能跳转到前一HOOK的.这一点,甚至在Jeffrey Richter的经典书籍<>里都描述错了.
正确的做法是:
#pragma data_seg("dt")
HHOOK prehook=0;
#pragma data_seg()
然后到VC的LINK OPITION里加上:
-SECTION:dt,RWS
这样,prehook就被搞到系统中被共享了.记住,一定要给prehook初始化.否则,MS编译器的编译器会LINK错误.
2.只有使用USER32.DLL的进程才会被INJECT.所以,HOOK并不是万能的.而且,用了USER32.DLL,也不一定会被INJECT.这里有个很好的例子就是整个OS启动中第一个被启动的WIN32进程:KERNEL32.DLL.大家很奇怪,KERNEL32.DLL是个DLL,怎么也会被作为进程加载?但是事实的确是这样的,顺便给大家再上一节98启动课吧.KERNEL32.DLL作为一个独立的进程,启动时加载了MSGSRV32.EXE.而MSGSRV32.EXE又启动了SPOOL32.EXE, SPOOL32.EXE启动了MPREXE.EXE.MPREXE.EXE可不能小看.我敢担保全中国没几个人真正知道它的作用的.MPREXE.EXE不仅是网络客户端部件启动的核心,更是WIN98的SERVICE的SCM.所有的WIN98的SERVICE都是由MPREXE.EXE启动的.WIN98的SERVICE都在HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices里呆者哩.大家都傻眼了吧.:DDD 有一点很令人奇怪, 那就是如果MPREXE.EXE运作不正常,那么SHELL绝对起不来.SHELL却是有MSGSRV32.EXE启动的.看来MPREXE和MSGSRV32有一套内部沟通机制啊.有了SHELL,就什么都有了.其他的东西被SHELL启动就难说准了.反正80%的程序是由SHELL启动的.好了,WIN98启动暂且讲到这里.我们继续原先的话题.KERNEL32.DLL居然就无法用HOOK入侵.大家如果不信的话,就试试看吧.
3.尽管使用USER32.DLL的进程会被INJECT. 但是这里还有一个技巧,那就是HOOK DLL是在进程第一次发出USER32调用的时候才被加载. 大家又目瞪口呆了吧.:) 这也就是说,在你发出USER32调用之前, HOOK DLL根本拿你没办法. 哇,真够幽默啊.:)))由于在启动HOOK前的进程绝对都是在调用GetMessage(...)/PeekMessage(...)中,那么HOOK DLL一下子满足了加载条件了.
4.加载HOOK时,HMODULE一定要正确,否则,系统就无法正确加载HOOK过程.千万不要用0代替HMODULE,因为0代表的是EXE映象的HINSTANCE(其实就是HMODULE).
好了.今天就写到这里.此文该算本主页里又一篇经典了吧.:)利用VB建立鼠标键盘操作回放
很多的教学软件或系统监视软件可以自动记录回放用户的输入文字或点击按钮等操作操作,这个功能的实现是使用
了Windows的Hook函数。本文介绍如何通过使用VB来实现鼠标键盘操作的纪录和回放。
Windows提供API函数SetwindowsHookEx来建立一个Hook,通过这个函数可以将一个程序添加到Hook链中监视Windows
消息,函数语法为:
Public Declare Function SetWindowsHookEx Lib "user32" _
用消息拦截技术制作系统日志
康帕斯(中国)国际信息服务有限公司 马文骞
01-6-7 下午 03:33:05
--------------------------------------------------------------------------------
能够完整记录电脑使用情况的日志文件在 Windows系统安全管理方面的作用是不可低估的。本文介绍了利用消息拦截技术制作日志文件的方法,其中的关键函数是一个未公开的 API系统调用。
一、利用钩子(Hook)拦截系统消息
日志文件对于一个大企业内部网络的维护与管理是至关重要的。另外还有许多其它场合也离不开日志的使用,例如:多人共享一台电脑,或在家庭中要记录儿童使用电脑的细节,等等。
日志程序若想完整记录电脑运行期间有哪些软件启动过、各使用了多长时间、以及使用浏览器访问互联网的情况等,必须对系统级消息进行拦截。RegisterShellHook是一个未公开的 API系统函数,它可以帮助日志程序在整个 Windows系统范围内感知到其它窗体的创建、激活或关闭等消息,而且不要求这些窗体与日志程序有父子关系,哪怕是 Windows最高级别的窗体也可以。RegisterShellHook 调用方法为:
Public Declare Function RegisterShellHook Lib "Shell32" Alias "#181" _
(ByVal hwnd As Long, ByVal nAction As Long) As Long
其中参数hwnd为日志程序的句柄,参数 nAction为所要进行操作的代码。具体的调用细节参见下面的例子及其注释。
二、将日志程序隐藏起来
把日志程序的Visible属性设为False当然是必要的一步。然后是 ShowInTaskbar属性也设为 False,以便其在 Windows的任务栏中不出现。最后,为了在 CTRL+ALT+DEL 所弹出的列表中隐藏日志程序,需要调用RegisterServiceProcess函数:
Public Declare Function RegisterServiceProcess Lib "kernel32" _
(ByVal dwProcessID As Long, ByVal dwType As Long) As Long
其中参数dwType是操作代码,值“1”表示从CTRL+ALT+DEL列表中去除,值“0”表示在列表中恢复;参数 dwProcessID是要在列表中去除或恢复的进程标识,可以用GetCurrentProcessId() API 函数得到日志程序的进程标识,也可以用更简便的方法,即把 dwProcessID参数置为空值,其含义是用当前程序的进程标识作为参数(见下例)。
另外,为了让日志程序在 Windows每次启动时都能自动运行,需要修改注册表,即在注册表的下述位置新建一个以日志程序的路径及名称为值的“串值”:
\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
此外,产生的日志文件也应妥为隐藏,最好用 Winsock控件随时向服务器传送。
为了简洁,下面的例子仅将日志文件放在了根目录,并且略去了用TCP/IP传送文件的代码。
三、一个完整的例子
下面的代码虽然短小,却是一个完整的能自我隐藏的日志程序(用 VB6.0实现,在 Win98下测试通过)。
' 窗体部分的代码(Form1.frm)
Option Explicit
Private Sub Form_Load()
Dim tmp As Long
' 将日志程序的名称从 CTRL+ALT+DEL 列表中清除
tmp = RegisterServiceProcess(ByVal 0&, 1)
Timer1.Interval = 60000 ' 定时器的作用是每隔一分钟将日志存盘
' 定义一个新的系统级的消息类型
Msg_ID = RegisterWindowMessage("SHELLHOOK")
Call RegisterShellHook(hwnd, 1) ' 调用未公开的函数(进行注册)
' 实施拦截:在存储了原入口地址的同时,将新地址指向自定义的函数WindowProc
Original = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf WindowProc)
End Sub
Private Sub Form_Unload(Cancel As Integer)
Dim tmp As Long
Call RegisterShellHook(hwnd, 0) ' 调用未公开的函数(取消注册)
tmp = SetWindowLong(hwnd, GWL_WNDPROC, Original) ' 将入口地址还原
End Sub
Private Sub Timer1_Timer()
If Len(Text1.Text) > 0 Then
Open "C:\SystemLog.Sys" For Append As #1 ' 以“添加”方式打开日志
Print #1, Text1.Text ' 日志自动存盘
Text1.Text = ""
Close #1
End If
End Sub
' 模块部分的代码(模块1.bas)
Public Declare Function RegisterShellHook Lib "Shell32" Alias "#181" _
(ByVal hwnd As Long, ByVal nAction As Long) As Long
Public Declare Function RegisterWindowMessage Lib "user32" Alias _
"RegisterWindowMessageA" (ByVal lpString As String) As Long
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" _
(ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
(ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
Public Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" _
(ByVal lpPrevWndFunc As Long, ByVal hwnd As Long, ByVal Msg As Long, ByVal _
wParam As Long, ByVal lParam As Long) As Long
Public Declare Function RegisterServiceProcess Lib "kernel32" _
(ByVal dwProcessID As Long, ByVal dwType As Long) As Long
Const HSHELL_WINDOWCREATED = 1 ' 系统级的窗体被创建
Const HSHELL_WINDOWDESTROYED = 2 ' 系统级的窗体即将被关闭
'Const HSHELL_ACTIVATESHELLWINDOW = 3 ' SHELL 的主窗体将被激活(本例未用)
Const HSHELL_WINDOWACTIVATED = 4 ' 系统级的窗体被激活
'Const HSHELL_GETMINRECT = 5 ' 窗体被最大化或最小化(本例未用)
'Const HSHELL_REDRAW = 6 ' Windows 任务栏被刷新(本例未用)
'Const HSHELL_TASKMAN = 7 ' 任务列表的内容被选中(本例未用)
'Const HSHELL_LANGUAGE = 8 ' 中英文切换或输入法切换(本例未用)
Public Const GWL_WNDPROC = -4 ' 该索引用来创建窗口类的子类
Public Msg_ID As Long, Original As Long
Public Function WindowProc(ByVal hwnd As Long, ByVal uMsg As Long, ByVal _
wParam As Long, ByVal lParam As Long) As Long ' 回调函数
Dim tmp1 As String, tmp2 As String, i As Long
If uMsg = Msg_ID Then
tmp1 = String(200, "*")
i = GetWindowText(lParam, tmp1, 200) ' 取窗体的标题
If i > 0 Then tmp1 = Left(tmp1, i) Else tmp1 = "未命名"
tmp1 = tmp1 + " " + Str(Date) + " " + Str(Time) + vbCrLf ' 加入日期
' 下面对窗体句柄值进行格式化的目的是为了日志文件在视觉上更美观
tmp2 = Format(lParam, "000000")
If Right(Form1.Text1, 2) <> vbCrLf Then tmp2 = vbCrLf + tmp2
Select Case wParam
Case HSHELL_WINDOWCREATED
Form1.Text1 = Form1.Text1 + tmp2 + " 创建:" + tmp1
Case HSHELL_WINDOWDESTROYED
Form1.Text1 = Form1.Text1 + tmp2 + " 关闭:" + tmp1
Case HSHELL_WINDOWACTIVATED
Form1.Text1 = Form1.Text1 + tmp2 + " 激活:" + tmp1
' 为了程序简洁,本例仅处理“创建”、“激活”和“关闭”这三个消息,
' 其实就生成日志文件的目的,上述三个消息已基本够用。
' Case ...
' ...
End Select
Else
' 使用已被存储下来的原入口地址
WindowProc = CallWindowProc(Original, hwnd, uMsg, wParam, lParam)
End If
End Function
下面列出的即为上述日志程序所产生的日志文件(长约十分钟的片段)。从中可以看出在该时间段内的电脑使用情况:曾拨号上网、浏览过“计算机世界”、收过邮件、访问过注册表等。左列的数字是相应窗体的句柄。
002624 激活:Project1 - Microsoft Visual Basic [设计]
002624 关闭:Microsoft Visual Basic [设计]
001692 创建:正在连接到 95963
001692 激活:正在连接到 95963
003512 关闭:Hotmail - 通行全球的免费 Web 电子邮件 - Microsoft Internet Explorer
001880 创建:未命名 01-6-6 16:01:25
001880 激活:未命名 01-6-6 16:01:25
001880 激活:计算机世界网-应用与方案-首页 - Microsoft Internet Explorer
001880 激活:计算机世界网-应用与方案-应用编程 - Microsoft Internet Explorer
003488 创建:Microsoft Internet Explorer 01-6-6 16:07:40
003488 激活:Microsoft Internet Explorer 01-6-6 16:07:41
003488 关闭:计算机世界网-用屏幕取词技术实现动态标注 - Microsoft Internet Explorer
001880 激活:计算机世界网-e海航标-首页 - Microsoft Internet Explorer
001880 关闭:计算机世界网-e海航标-首页 - Microsoft Internet Explorer
001132 激活:浏览 - C:\
001132 关闭:浏览 - C:\
002772 创建:Outlook Express 01-6-6 16:10:41
002772 激活:Outlook Express 01-6-6 16:10:41
002772 激活:收件箱 - Outlook Express
002772 关闭:收件箱 - Outlook Express
003920 关闭:浏览 - 我的电脑
000640 创建:注册表编辑器
000640 激活:注册表编辑器
000640 关闭:注册表编辑器
003756 创建:未命名 01-6-6 16:11:30
003756 关闭:未命名 01-6-6 16:11:30
001328 创建:网络监视器
001328 激活:网络监视器
001328 激活:网络监视器 - 0 连接到 \\CD_PROGRAM
001328 关闭:网络监视器 - 0 连接到 \\CD_PROGRAM
002700 关闭:连接到 95963
001804 关闭:未命名 01-6-6 16:13:13
Platform SDK: Interprocess Communications
Monitoring System Events
The following example uses a variety of thread-specific hook procedures to monitor the system for events affecting a thread. It demonstrates how to process events for the following types of hook procedures:
WH_CALLWNDPROC
WH_CBT
WH_DEBUG
WH_GETMESSAGE
WH_KEYBOARD
WH_MOUSE
WH_MSGFILTER
The user can install and remove a hook procedure by using the menu. When a hook procedure is installed and an event that is monitored by the procedure occurs, the procedure writes information about the event to the client area of the application's main window.
#define NUMHOOKS 7
// Global variables
typedef struct _MYHOOKDATA
{
int nType;
HOOKPROC hkprc;
HHOOK hhook;
} MYHOOKDATA;
MYHOOKDATA myhookdata[NUMHOOKS];
LRESULT WINAPI MainWndProc(HWND hwndMain, UINT uMsg, WPARAM wParam,
LPARAM lParam)
{
static BOOL afHooks[NUMHOOKS];
int index;
static HMENU hmenu;
switch (uMsg)
{
case WM_CREATE:
// S***e the menu handle.
hmenu = GetMenu(hwndMain);
// Initialize structures with hook data. The menu-item
// identifiers are defined as 0 through 6 in the
// header file. They can be used to identify array
// elements both here and during the WM_COMMAND
// message.
myhookdata[IDM_CALLWNDPROC].nType = WH_CALLWNDPROC;
myhookdata[IDM_CALLWNDPROC].hkprc = CallWndProc;
myhookdata[IDM_CBT].nType = WH_CBT;
myhookdata[IDM_CBT].hkprc = CBTProc;
myhookdata[IDM_DEBUG].nType = WH_DEBUG;
myhookdata[IDM_DEBUG].hkprc = DebugProc;
myhookdata[IDM_GETMESSAGE].nType = WH_GETMESSAGE;
myhookdata[IDM_GETMESSAGE].hkprc = GetMsgProc;
myhookdata[IDM_KEYBOARD].nType = WH_KEYBOARD;
myhookdata[IDM_KEYBOARD].hkprc = KeyboardProc;
myhookdata[IDM_MOUSE].nType = WH_MOUSE;
myhookdata[IDM_MOUSE].hkprc = MouseProc;
myhookdata[IDM_MSGFILTER].nType = WH_MSGFILTER;
myhookdata[IDM_MSGFILTER].hkprc = MessageProc;
// Initialize all flags in the array to FALSE.
memset(afHooks, FALSE, sizeof(afHooks));
return 0;
case WM_COMMAND:
switch (LOWORD(wParam))
{
// The user selected a hook command from the menu.
case IDM_CALLWNDPROC:
case IDM_CBT:
case IDM_DEBUG:
case IDM_GETMESSAGE:
case IDM_KEYBOARD:
case IDM_MOUSE:
case IDM_MSGFILTER:
// Use the menu-item identifier as an index
// into the array of structures with hook data.
index = LOWORD(wParam);
// If the selected type of hook procedure isn't
// installed yet, install it and check the
// associated menu item.
if (!afHooks[index])
{
myhookdata[index].hhook = SetWindowsHookEx(
myhookdata[index].nType,
myhookdata[index].hkprc,
(HINSTANCE) NULL, GetCurrentThreadId());
CheckMenuItem(hmenu, index,
MF_BYCOMMAND | MF_CHECKED);
afHooks[index] = TRUE;
}
// If the selected type of hook procedure is
// already installed, remove it and remove the
// check mark from the associated menu item.
else
{
UnhookWindowsHookEx(myhookdata[index].hhook);
CheckMenuItem(hmenu, index,
MF_BYCOMMAND | MF_UNCHECKED);
afHooks[index] = FALSE;
}
default:
return (DefWindowProc(hwndMain, uMsg, wParam,
lParam));
}
break;
//
// Process other messages.
//
default:
return DefWindowProc(hwndMain, uMsg, wParam, lParam);
}
return NULL;
}
/****************************************************************
WH_CALLWNDPROC hook procedure
****************************************************************/
LRESULT WINAPI CallWndProc(int nCode, WPARAM wParam, LPARAM lParam)
{
CHAR szCWPBuf[256];
CHAR szMsg[16];
HDC hdc;
static int c = 0;
int cch;
if (nCode < 0) // do not process message
return CallNextHookEx(myhookdata[CALLWNDPROC].hhook, nCode,
wParam, lParam);
// Call an application-defined function that converts a message
// constant to a string and copies it to a buffer.
LookUpTheMessage((PMSG) lParam, szMsg);
hdc = GetDC(hwndMain);
switch (nCode)
{
case HC_ACTION:
cch = wsprintf(szCWPBuf,
"CALLWNDPROC - tsk: %ld, msg: %s, %d times ",
wParam, szMsg, c++);
TextOut(hdc, 2, 15, szCWPBuf, cch);
break;
default:
break;
}
ReleaseDC(hwndMain, hdc);
return CallNextHookEx(myhookdata[CALLWNDPROC].hhook, nCode,
wParam, lParam);
}
/****************************************************************
WH_GETMESSAGE hook procedure
****************************************************************/
LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
CHAR szMSGBuf[256];
CHAR szRem[16];
CHAR szMsg[16];
HDC hdc;
static int c = 0;
int cch;
if (nCode < 0) // do not process message
return CallNextHookEx(myhookdata[GETMESSAGE].hhook, nCode,
wParam, lParam);
switch (nCode)
{
case HC_ACTION:
switch (wParam)
{
case PM_REMOVE:
lstrcpy(szRem, "PM_REMOVE");
break;
case PM_NOREMOVE:
lstrcpy(szRem, "PM_NOREMOVE");
break;
default:
lstrcpy(szRem, "Unknown");
break;
}
// Call an application-defined function that converts a
// message constant to a string and copies it to a
// buffer.
LookUpTheMessage((PMSG) lParam, szMsg);
hdc = GetDC(hwndMain);
cch = wsprintf(szMSGBuf,
"GETMESSAGE - wParam: %s, msg: %s, %d times ",
szRem, szMsg, c++);
TextOut(hdc, 2, 35, szMSGBuf, cch);
break;
default:
break;
}
ReleaseDC(hwndMain, hdc);
return CallNextHookEx(myhookdata[GETMESSAGE].hhook, nCode,
wParam, lParam);
}
/****************************************************************
WH_DEBUG hook procedure
****************************************************************/
LRESULT CALLBACK DebugProc(int nCode, WPARAM wParam, LPARAM lParam)
{
CHAR szBuf[128];
HDC hdc;
static int c = 0;
int cch;
if (nCode < 0) // do not process message
return CallNextHookEx(myhookdata[DEBUG].hhook, nCode,
wParam, lParam);
hdc = GetDC(hwndMain);
switch (nCode)
{
case HC_ACTION:
cch = wsprintf(szBuf,
"DEBUG - nCode: %d, tsk: %ld, %d times ",
nCode,wParam, c++);
TextOut(hdc, 2, 55, szBuf, cch);
break;
default:
break;
}
ReleaseDC(hwndMain, hdc);
return CallNextHookEx(myhookdata[DEBUG].hhook, nCode, wParam,
lParam);
}
/****************************************************************
WH_CBT hook procedure
****************************************************************/
LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam)
{
CHAR szBuf[128];
CHAR szCode[128];
HDC hdc;
static int c = 0;
int cch;
if (nCode < 0) // do not process message
return CallNextHookEx(myhookdata[CBT].hhook, nCode, wParam,
lParam);
hdc = GetDC(hwndMain);
switch (nCode)
{
case HCBT_ACTIVATE:
lstrcpy(szCode, "HCBT_ACTIVATE");
break;
case HCBT_CLICKSKIPPED:
lstrcpy(szCode, "HCBT_CLICKSKIPPED");
break;
case HCBT_CREATEWND:
lstrcpy(szCode, "HCBT_CREATEWND");
break;
case HCBT_DESTROYWND:
lstrcpy(szCode, "HCBT_DESTROYWND");
break;
case HCBT_KEYSKIPPED:
lstrcpy(szCode, "HCBT_KEYSKIPPED");
break;
case HCBT_MINMAX:
lstrcpy(szCode, "HCBT_MINMAX");
break;
case HCBT_MOVESIZE:
lstrcpy(szCode, "HCBT_MOVESIZE");
break;
case HCBT_QS:
lstrcpy(szCode, "HCBT_QS");
break;
case HCBT_SETFOCUS:
lstrcpy(szCode, "HCBT_SETFOCUS");
break;
case HCBT_SYSCOMMAND:
lstrcpy(szCode, "HCBT_SYSCOMMAND");
break;
default:
lstrcpy(szCode, "Unknown");
break;
}
cch = wsprintf(szBuf, "CBT - nCode: %s, tsk: %ld, %d times ",
szCode, wParam, c++);
TextOut(hdc, 2, 75, szBuf, cch);
ReleaseDC(hwndMain, hdc);
return CallNextHookEx(myhookdata[CBT].hhook, nCode, wParam,
lParam);
}
/****************************************************************
WH_MOUSE hook procedure
****************************************************************/
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
CHAR szBuf[128];
CHAR szMsg[16];
HDC hdc;
static int c = 0;
int cch;
if (nCode < 0) // do not process the message
return CallNextHookEx(myhookdata[MOUSE].hhook, nCode,
wParam, lParam);
// Call an application-defined function that converts a message
// constant to a string and copies it to a buffer.
LookUpTheMessage((PMSG) lParam, szMsg);
hdc = GetDC(hwndMain);
cch = wsprintf(szBuf,
"MOUSE - nCode: %d, msg: %s, x: %d, y: %d, %d times ",
nCode, szMsg, LOWORD(lParam), HIWORD(lParam), c++);
TextOut(hdc, 2, 95, szBuf, cch);
ReleaseDC(hwndMain, hdc);
return CallNextHookEx(myhookdata[MOUSE].hhook, nCode, wParam,
lParam);
}
/****************************************************************
WH_KEYBOARD hook procedure
****************************************************************/
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
CHAR szBuf[128];
HDC hdc;
static int c = 0;
int cch;
if (nCode < 0) // do not process message
return CallNextHookEx(myhookdata[KEYBOARD].hhook, nCode,
wParam, lParam);
hdc = GetDC(hwndMain);
cch = wsprintf(szBuf, "KEYBOARD - nCode: %d, vk: %d, %d times ",
nCode, wParam, c++);
TextOut(hdc, 2, 115, szBuf, cch);
ReleaseDC(hwndMain, hdc);
return CallNextHookEx(myhookdata[KEYBOARD].hhook, nCode, wParam,
lParam);
}
/****************************************************************
WH_MSGFILTER hook procedure
****************************************************************/
LRESULT CALLBACK MessageProc(int nCode, WPARAM wParam, LPARAM lParam)
{
CHAR szBuf[128];
CHAR szMsg[16];
CHAR szCode[32];
HDC hdc;
static int c = 0;
int cch;
if (nCode < 0) // do not process message
return CallNextHookEx(myhookdata[MSGFILTER].hhook, nCode,
wParam, lParam);
switch (nCode)
{
case MSGF_DIALOGBOX:
lstrcpy(szCode, "MSGF_DIALOGBOX");
break;
case MSGF_MENU:
lstrcpy(szCode, "MSGF -
软件测试自动化实践
2008-9-11
很高兴今天有机会和大家讨论一下软件测试自动化的实践,今天的话题分为两个部分 :一是软件自动化功能测试;还有一部分会介绍一下软件自动化性能测试。实践主要包含两个部分:一部分是介绍HP在软件的功能和性能自动化测试的理念,以及产品和技术在这方面的支持。另一部分是一些实践案例,包括在国内外哪些用户使用我们的测试工具,他们是如何去做的。
首先是自动化功能测试,我们讨论一下他的适用范围,或使用时机是何时。对于一个新的项目,比如项目周期很紧的功能测试项目,如果临时分配30个人,按测试方案进行手工测试的效率可能要比自动化测试工具录制脚本在测试的效率好的多。
那么自动化测试工具的价值在什么地方?我们可以看一下,很多客户如果想增加一些新的功能,或者是修复bug,经常会推出产品新的版本,在推出的过程中,我们也知道,除了测试修改过的模块外,每次都要重复测试有关联的模块,这样很多时候会做大量的重复工作,人员很疲惫也达不到测试效果,自动化功能测试工具就可以创建整个测试生命周期的可重用模块,同时还能覆盖大部分的系统测试,更主要的是录制好脚本以后,自动去执行,机器去操作,减少了人为主观的错误,同时使测试人员解脱出来,专注新的模块。自动化测试最大的价值在于回归测试。在产品提交过来之后要执行“冒烟测试”,自动化测试工具能够节省时间和金钱。图中是国际上某金融机构的统计,在过去三年内使用使用自动化测试工具的投资回报率达到1500%。
下面我们看一下自动化测试的原理是什么。自动化测试发展到现在,很多厂商走的技术路线都是类似的,一是通过录制生成脚本,业务人员或测试人员按正常的业务执行流程,同时自动化工具录制并生成脚本,要注意的是,它录制的不是鼠标和键盘的操作,而是对象的操作,如某个button被click了一下,或某个文本框输入了数据,这样的好处是当button位置发生了变化,脚本会根据对象的属性精确定位到对象,然后进行脚本回放,可以不需要反复修改,来执行自动化测试。当录制好脚本后,可能要执行测试数据的参数化。录制的可能是一套数据,如录制登录操作,可能录制的是正确的用户名和密码,但实际执行测试的时候可能需要很多的组合,比如正确的用户名、错误的密码,空用户名、空密码等等,这时候你需要对输入的数据进行参数化。那么需要这种参数表对参数进行定义。
接下来第三点是自动化测试以功能测试是否正确作为结果来判断的,它需要定义正确的检查点,就是说我能够通过对象的属性或界面的文字去判断我的功能执行是不是正确的。还有一个比较重要的,也是很多朋友容易忽略的,就是最后的测试报告。测试运行完以后,我需要根据我的检查点去判断我的运行结果怎样,有的产品的报告可能是文字型的,但实际上对于很多测试需要图形化的报告,可以看到我的检查点是什么,执行的时候是什么情况,为什么会出现错误。
基于以上这些,我们可以看到当使用软件测试自动化工具的时候需要考虑什么问题:
1.工具要有对对象很好的识别和维护的能力,支持各种传统和新的技术,象今天上午的调研报告里反映的,很多朋友也都关心自动化测试工具是否支持一些新的技术,另外,还需要统一的对象库,脚本可以基于对象库统一管理对象,当对象库的某一对象发生变化时,如中文版的button上是“确定”,英文版可能就是“ok”,可以更改对象库中对象的属性,就不需要打开每个脚本进行修改,还有一个比较重要的是对象库的图形化操作,可能会有些对象库的合并、分拆等等。
2.就是脚本要易于修改和维护,不仅仅是脚本的语言,更主要的是要提供脚本图形化的编辑。虽然测试人员很多是从开发人员转换过来的,但测试人员不等于开发人员,那么工具的使用是要易于理解和掌握,像有的用户甚至是业务人员和开发人员一起录制脚本,这时候工具对于业务人员要易于理解和掌握,要能够知道工具是如何用的。还有就是离线编辑,不用起应用也能够进行脚本编辑。
3.测试数据的驱动,数据表要易于编辑和维护,数据参数化操作。
4.检查点,支持多种检查点,如对象、文本、位图。
5.结果报告,刚才我们也提到,结果报告的图形化也是很有必要的,而且要易于浏览。下面我们看看惠普提供的相关功能测试套件,上午的调研报告中我很高兴的看到,对自动化测试工具在企业中应用的调查结果,WinRunner是排在第一位的,但实际上HP同时还有另外一个自动化测试工具产品,就是Quit Test Professional,即QTP。
目前,HP是把WinRunner和QTP作为整体的功能测试套件提供。WinRunner比较关注传统的应用,如早期的Delphi、PowerBuilder;QTP关注新兴技术,如.NET、新的WebService、无线、VMWare桌面支持的测试,QTP都可以支持。同时这两个产品又有通用的覆盖面,包括像Web、VB、Activex等等,但我们向用户推荐QTP,因为它具有灵活、易用、简单的特性。它不仅提供脚本语言的编辑,同时还提供关键字视图界面。在界面中它对每一个对象进行梳理、提取,同时下面又提供数据表和实时的捕捉,这样用户可以很方便的编辑。同时也可以把对象选择进来,离线编辑脚本。另外还提供ActiveScreen技术,可以界面快照,然后对快照添加验证点、测试步骤,甚至离线编辑。
下面是我们在长期的工作过程中总结的自动化功能测试的原则:
1.就是选择合适的被测应用;
2.就是要选择合适的案例;
3.设计自动化功能测试框架;
4.自动化测试实际上是这种规模效应,覆盖率达到一定规模,他的效果才能体现出来,同时也是要不断积累和完善的。
1.选择合适的被测应用:
• 工具对应用界面开发技术的支持程度
• 生命周期长,但是经常变更和升级
• 界面变化相对不大
• 开发已经基本完成
• 回归测试阶段 − 检查已知错误是否重现 − 发现修改造成的新错误
2.选择合适的案例
• 高业务风险
• 手工测试复杂度高
• 实现自动化测试难度低
• 前期测试发现缺陷比较多案例评估方法这个片子是介绍如何评估业务的风险。主要从业务风险评估和技术风险评估两方面来说。从不同的维度来评估你的业务是不是具有高风险。
3.规模效应,不断完善积累
• 设计先行
• 覆盖率越高,价值越明显
• 覆盖率和投入成正比
• 不要一开始就期望高覆盖率
• 逐步使用,逐步发展,逐步完善
另外一方面,对于自动化功能测试仅仅录制回放是不够的:
• 设计自动化功能测试框架
• 业务人员和技术人员的协同工作
• 大批量脚本的调度 • 重用需要实现脚本调度
• 数据驱动的要求 • 界面一旦变化的维护要求
4.自动化功能测试设计框架这里我们提出的自动化功能测试设计框架应该包含的内容,首先最关键的是中心管理,我们首先应该有自己的库(Central Management),去集中管理所有的自动化测试脚本;上面一层是功能库(Functional Lib),是一些可以提取的函数;再上面一层是业务组件(Logic Components),把被测系统可重用的组件提取出来;再上面一层是控制器(Controller),可以控制、组织业务组件,形成一个个业务流程;再上面是调用的脚本(Load scrīpting),实现脚本的调度。
下面我们来看一下,传统的自动化功能测试是序列化的,从登录、创建订单、查看订单到退出,是一步步做的,数据往往和脚本是捆绑在一起的,对脚本的调用还是需要用写代码的方式来维护。
而HP的业务流程测试(Business Process Testing)—基于Web的无测试脚本的功能测试,它与传统的自动化功能测试的区别是:
• 使业务人员参与自动化功能测试的设计和使用,及早进行测试规划
• 业务人员使用自然语言定义组件;测试人员脚本实现
• 使用应用界面流和数据创建测试案例 • 大量减少自动化测试案例维护时间
• QTP/WR与TD for QC集成实现
外面的展厅中,我的同事会有一些实时的demo展示,大家如果感兴趣可以在间歇的时候去看一下,业务流程测试怎么样方便的帮助用户实现自动化功能测试的框架。这个图是HP的质量中心的框架图,在软件质量管理讲演中会对它详细介绍,这里我就不详细介绍了。
第二部分给大家介绍软件自动化性能测试。讲解之前,我首先问大家一个问题,有多少人用过LoadRunner?好,我本来想如果用的人多的话我就着重介绍一些新的特性,现在看来大家用的不多,我还是详细介绍一下。
前面我们介绍的是功能测试,主要是在功能上看产品和业务的对应情况,能不能满足业务需求。但同时我们也知道产品的使用往往不是一两个人,少则几十,多则上千,那么产品上线以后是不是能够支撑这么多用户,因此要做性能测试,他与功能测试还有个区别是,功能测试还是可以靠人力去做,但性能测试往往无法靠人力做的,因为没有办法做到这么多人同时做一个操作,并计算响应时间。
作为性能测试,我们往往面临一些问题:
1.性能测试目标不明确;
2.业务部门和测试团队缺乏通用语言;
3.脚本能否录制和回放;
4.场景如何接近真实地模拟;
5.瓶颈定位。
HP LoadRunner作为业界领先的自动化压力测试工具,它具有很多功能:
1.使用成百上千的虚拟用户代替真实用户;
2.从单一控制点对系统产生精确的,可测量和可重复的负载,并且提供无代理的监控;
3.强大的分析器,协助查明系统瓶颈。
然后我们可以看一下LoadRunner如何工作?
• 将业务流程录制为自动化脚本,例如股票交易应用中的 “买入”;
• 添加事务, 参数化输入数据, 添加验证点;
• 模拟用户行为,例如网络连接类型,频率等…
-
GC的工作原理
2008-9-10
垃圾收集器(Garbage Collector,GC)对Java程序员来说,基本 上是透明的,但是一个优秀的Java程序员必须了解GC的工作原理、如何优化GC的性能、如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统、实时系统等,只有全面提升内存的管理效率 ,才能提高整个应用程序的性能。本篇文章首先简单介绍GC的工作原理之后,然后再对GC的几个关键问题进行深入探讨,最后提出一些Java程序设计建议,从GC角度提高Java程序的性能。
Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。
对于程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为"不可达的"。GC将负责回收所有"不可达"对象的内存空间。
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象(详见 参考资料1 )。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。但是,为了保证GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都没有明确的规定。因此,不同的JVM的实现者往往有不同的实现算法。这也给Java程序员的开发带来行多不确定性。本文研究了几个与GC工作相关的问题,努力减少这种不确定性给Java程序带来的负面影响。
GC在JVM中通常是由一个或一组进程来实现的,它本身也和用户程序一样占用heap空间,运行时也占用CPU。当GC进程运行时,应用程序停止运行。因此,当GC运行时间较长时,用户能够感到Java程序的停顿,另外一方面,如果GC运行时间太短,则可能对象回收率太低,这意味着还有很多应该回收的对象没有被回收,仍然占用大量内存。因此,在设计GC的时候,就必须在停顿时间和回收率之间进行权衡。一个好的GC实现允许用户定义自己所需要的设置,例如有些内存有限有设备,对内存的使用量非常敏感,希望GC能够准确的回收内存,它并不在意程序速度的放慢。另外一些实时网络游戏,就不能够允许程序有长时间的中断。增量式GC就是通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。虽然,增量式GC在整体性能上可能不如普通GC的效率高,但是它能够减少程序的最长停顿时间。
下图就表示了,增量式GC和普通GC的比较。其中灰色部分表示线程占用CPU的时间。
Sun JDK提供的HotSpot JVM就能支持增量式GC。HotSpot JVM缺省GC方式为不使用增量GC,为了启动增量GC,我们必须在运行Java程序时增加-Xincgc的参数。HotSpot JVM增量式GC的实现是采用Train GC算法。它的基本想法就是,将堆中的所有对象按照创建和使用情况进行分组(分层),将使用频繁高和具有相关性的对象放在一队中,随着程序的运行,不断对组进行调整。当GC运行时,它总是先回收最老的(最近很少访问的)的对象,如果整组都为可回收对象,GC将整组回收。这样,每次GC运行只回收一定比例的不可达对象,保证程序的顺畅运行。Train GC算法是一个非常好的算法,具体算法见 参考资料4。
finalize是位于Object类的一个方法,该方法的访问修饰符为protected,由于所有类为Object的子类,因此用户类很容易访问到这个方法。由于,finalize函数没有自动实现链式调用,我们必须手动的实现,因此finalize函数的最后一个语句通常是super.finalize()。通过这种方式,我们可以实现从下到上实现finalize的调用,即先释放自己的资源,然后再释放父类的资源。
根据Java语言规范,JVM保证调用finalize函数之前,这个对象是不可达的,但是JVM不保证这个函数一定会被调用。另外,规范还保证finalize函数最多运行一次。
很多Java初学者会认为这个方法类似与C++中的析构函数,将很多对象、资源的释放都放在这一函数里面。其实,这不是一种很好的方式。原因有三,其一,GC为了能够支持finalize函数,要对覆盖这个函数的对象作很多附加的工作。其二,在finalize运行完成之后,该对象可能变成可达的,GC还要再检查一次该对象是否是可达的。因此,使用finalize会降低GC的运行性能。其三,由于GC调用finalize的时间是不确定的,因此通过这种方式释放资源也是不确定的。
通常,finalize用于一些不容易控制、并且非常重要资源的释放,例如一些I/O的操作,数据的连接。这些资源的释放对整个应用程序是非常关键的。在这种情况下,程序员应该以通过程序本身管理(包括释放)这些资源为主,以finalize函数释放资源方式为辅,形成一种双保险的管理机制,而不应该仅仅依靠finalize来释放资源。
下面给出一个例子说明,finalize函数被调用以后,仍然可能是可达的,同时也可说明一个对象的finalize只可能运行一次。
class MyObject{ Test main; //记录Test对象,在finalize中时用于恢复可达性 public MyObject(Test t) { main=t; //保存Test 对象 } protected void finalize() { main.ref=this;// 恢复本对象,让本对象可达 System.out.println("This is finalize");//用于测试finalize只运行一次 } } class Test { MyObject ref; public static void main(String[] args) { Test test=new Test(); test.ref=new MyObject(test); test.ref=null; //MyObject对象为不可达对象,finalize将被调用 System.gc(); if (test.ref!=null) System.out.println("My Object还活着"); } }
运行结果:
This is finalize
MyObject还活着此例子中,需要注意的是虽然MyObject对象在finalize中变成可达对象,但是下次回收时候,finalize却不再被调用,因为finalize函数最多只调用一次。
Java2增强了内存管理功能, 增加了一个java.lang.ref包,其中定义了三种引用类。这三种引用类分别为SoftReference、WeakReference和PhantomReference。通过使用这些引用类,程序员可以在一定程度与GC进行交互,以便改善GC的工作效率。这些引用类的引用强度介于可达对象和不可达对象之间。它们的引用强度如下图所示:
创建一个引用对象也非常容易,例如如果你需要创建一个Soft Reference对象,那么首先创建一个对象,并采用普通引用方式(可达对象);然后再创建一个SoftReference引用该对象;最后将普通引用设置为null。通过这种方式,这个对象就只有一个Soft Reference引用。同时,我们称这个对象为Soft Reference 对象。
Soft Reference的主要特点是据有较强的引用功能。只有当内存不够的时候,才进行回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用图片的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。以下给出这种引用类型的使用伪代码;
//申请一个图像对象 Image image=new Image();//创建Image对象 … //使用 image … //使用完了image,将它设置为soft 引用类型,并且释放强引用; SoftReference sr=new SoftReference(image); image=null; … //下次使用时 if (sr!=null) image=sr.get(); else{ //由于GC由于低内存,已释放image,因此需要重新装载; image=new Image(); sr=new SoftReference(image); }
Weak引用对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象,GC总是进行回收。Weak引用对象更容易、更快被GC回收。虽然,GC在运行时一定回收Weak对象,但是复杂关系的Weak对象群常常需要好几次GC的运行才能完成。Weak引用对象常常用于Map结构中,引用数据量较大的对象,一旦该对象的强引用为null时,GC能够快速地回收该对象空间。该例子见 参考资料4;
Phantom引用的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些对象,它们执行完了finalize函数,并为不可达对象,但是它们还没有被GC回收。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。
根据GC的工作原理,我们可以通过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。以下就是一些程序设计的几点建议。
- 最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null。我们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC回收它们一般效率较低。如果程序允许,尽早将不用的引用对象赋为null。这样可以加速GC的工作。
- 尽量少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。
- 如果需要使用经常使用的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory。
- 注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象(dangling reference),造成内存浪费。
- 当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。使用增量式GC可以缩短Java程序的暂停时间。
文章
- 欧阳辰,周欣 "Java与内存泄漏" http://www.ibm.com/developerworks/cn/java/l-JavaMemoryLeak/index.shtml
- Y. Srinivas Ramakrishna "Atuomatic Memory Management in the Java HotSpot Virtual Machine",此文章JavaOne2002的演讲材料, http://java.sun.com/javaone
- Monica Pawlan "Reference Objects and Garbage Collector" 此文章为JDC的文章,可在 http://developer.java.sun.com/上找到
- Bill Venners Chapter 9 of "Inside the Java 2 Virtual Machine" http://www.artima.com/insidejvm/ed2/ch09GarbageCollectionPrint.html
- Sun Microsystems, "Java Language Specification, Second Version"


欧阳辰,北京大学计算机硕士毕业,98年起开始研究基于java的软件开发、测试,参与开发、测试过多个基于Java的应用程序和Web服务项目。联系方式 yeekee@sina.com


周欣,北京大学计算机系在读博士生,主要研究方向:程序理解、逆向工程及软件度量,联系方式 zhouxin@sei.pku.edu.cn。
-
JAVA中的反射机制详解
2008-9-10
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。Java反射机制主要提供了以下功能: 在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。
1. 得到某个对象的属性
public Object getProperty(Object owner, String fieldName) throws Exception {
Class ōwnerClass = owner.getClass();
Field field = ownerClass.getField(fieldName);
Object property = field.get(owner);
return property;
}
Class ōwnerClass = owner.getClass():得到该对象的Class。
Field field = ownerClass.getField(fieldName):通过Class得到类声明的属性。
Object property = field.get(owner):通过对象得到该属性的实例,如果这个属性是非公有的,这里会报IllegalAccessException。
2. 得到某个类的静态属性
public Object getStaticProperty(String className, String fieldName)
throws Exception {
Class ōwnerClass = Class.forName(className);
Field field = ownerClass.getField(fieldName);
Object property = field.get(ownerClass);
return property;
}
Class ōwnerClass = Class.forName(className) :首先得到这个类的Class。
Field field = ownerClass.getField(fieldName):和上面一样,通过Class得到类声明的属性。
Object property = field.get(ownerClass) :这里和上面有些不同,因为该属性是静态的,所以直接从类的Class里取。
3. 执行某对象的方法
public Object invokeMethod(Object owner, String methodName, Object[] args) throws Exception {
Class ōwnerClass = owner.getClass();
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Method method = ownerClass.getMethod(methodName, argsClass);
return method.invoke(owner, args);
}
Class owner_class = owner.getClass() :首先还是必须得到这个对象的Class。
5~9行:配置参数的Class数组,作为寻找Method的条件。
Method method = ownerClass.getMethod(methodName, argsClass):通过Method名和参数的Class数组得到要执行的Method。
method.invoke(owner, args):执行该Method,invoke方法的参数是执行这个方法的对象,和参数数组。返回值是Object,也既是该方法的返回值。4. 执行某个类的静态方法
public Object invokeStaticMethod(String className, String methodName,
Object[] args) throws Exception {
Class ōwnerClass = Class.forName(className);
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Method method = ownerClass.getMethod(methodName, argsClass);
return method.invoke(null, args);
}
基本的原理和实例3相同,不同点是最后一行,invoke的一个参数是null,因为这是静态方法,不需要借助实例运行。
5. 新建实例
public Object newInstance(String className, Object[] args) throws Exception {
Class newoneClass = Class.forName(className);
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
argsClass[i] = args[i].getClass();
}
Constructor cons = newoneClass.getConstructor(argsClass);
return cons.newInstance(args);
}
这里说的方法是执行带参数的构造函数来新建实例的方法。如果不需要参数,可以直接使用newoneClass.newInstance()来实现。
Class newoneClass = Class.forName(className):第一步,得到要构造的实例的Class。
第5~第9行:得到参数的Class数组。
Constructor cons = newoneClass.getConstructor(argsClass):得到构造子。
cons.newInstance(args):新建实例。
6. 判断是否为某个类的实例
public boolean isInstance(Object obj, Class cls) {
return cls.isInstance(obj);
}
7. 得到数组中的某个元素
public Object getByArray(Object array, int index) {
return Array.get(array,index);
}
