《WINDOWS 环境下32位汇编语言程序设计》第6章 定时器和Windows时间
6.1 定时器
在DOS操作系统中要用到定时功能的时候一般有两种方法,一种是用一个空循环来延时;二是截获时钟中断,计算机的硬件时钟中断会以每55 ms一次的频率触发8号中断,而在默认的int 08h中断处理程序中有一句调用int 1ch的代码,所以截获int 08h或int 1ch都可以达到定时的要求。第一种方法的定时效果随计算机主频的不同可能会大不相同,相比之下,第二种方法更为常用。
在Windows操作系统下,用户程序不可能去截获时钟中断,所以操作系统用提供定时器的方法来满足用户的类似需求。
6.1.1 定时器简介
在应用程序需要使用定时器时,可以用SetTimer函数向Windows申请一个定时器,要求系统在指定的时间以后“通知”应用程序,如果申请成功的话,系统会以指定的时间周期调用SetTimer函数指定的回调函数,或者向指定的窗口过程发送WM_TIMER消息,与DOS操作系统固定以55 ms的间隔触发中断服务程序相比,SetTimer函数可以指定的时间间隔更为灵活——以ms为单位,可以指定的时间周期为一个32位的整数,也就是从1~4294967295 ms,这可是一个将近50天的范围!
但是在具体的使用中不要被这个参数所迷惑:由于Windows的定时器同样是基于时钟中断的,所以虽然参数的单位是ms,但精度还是55 ms,如果指定一个小于55 ms的周期,不管是1 ms还是54 ms,Windows最快也只能在每个时钟中断的时候触发这个定时器,也就是说,实际上这个定时器是以55 ms为触发周期的;另外,当指定一个时间间隔的时候,Windows以与这个间隔最接近的55 ms的整数倍时间来触发定时器,假定建立一个周期为1000 ms的定时器,定时器的触发周期实际上不是1 s而是989 ms(55ms×18)。
使用定时器时还有一个要点就是定时器消息是一个低级别的消息,这表现在两个方面:首先,Windows只有在消息队列中没有其他消息的情况下才会发送WM_TIMER消息,如果窗口过程忙于处理某个消息没有返回,使消息队列中有消息积累起来,那么WM_TIMER消息就会被丢弃,在消息队列再度空闲的时候,被丢弃的WM_TIMER消息不会被补发(用一句经典的话来描述就是:“过去的就让它过去吧!”);其次,消息队列中不会有多条WM_TIMER消息,如果消息队列中已经有一条WM_TIMER消息,还没来得及处理,又到了定时的时刻,那么两条WM_TIMER消息会被合并成一条。
由此可见,应用程序不能依靠定时器来保证某件事情必须在规定的时刻被处理,另外,也不能依赖对定时器消息计数来确定已经过去了多少时间。
读者可以在所附光盘的Chapter06\Timer目录中找到一个例子,运行Timer.exe以后出现的界面如图6.1所示。
图6.1 定时器例子的运行结果
这个例子程序中共定义了3个定时器,第1个以250 ms为周期更换对话框上的图标;第2个以1s为单位进行计数并把结果显示在对话框上;第3个以2s为单位驱动扬声器发出“嘟嘟”的响声。为了验证WM_TIMER消息的级别,读者可以在运行中按住标题栏的“关闭”按钮不放,就可以发现3个定时器全部停止了,然后将鼠标移出“关闭”按钮并释放,定时器会重新工作,但对话框上的计数结果在定时器停止的期间并没有补上去,也就是说,在这期间,WM_TIMER消息被全部丢弃了。
6.1.2 定时器的使用方法
下面以Timer程序为例说明定时器的使用方法,这个程序的资源脚本文件定义如下:
#include <c:/masm32/include/resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define DLG_MAIN 1
#define ICO_1 1
#define ICO_2 2
#define IDC_SETICON 100
#define IDC_COUNT 101
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_1 ICON "1.ico"
ICO_2 ICON "2.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 50, 50, 113, 40
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "定时器例子"
FONT 9, "宋体"
{ICON ICO_1, IDC_SETICON, 8, 9, 18, 21LTEXT "计数:", -1, 35, 16, 25, 10LTEXT "", IDC_COUNT, 62, 16, 40, 10
}
对资源的定义读者现在一定不会陌生了,这个文件中定义了两个图标和一个对话框,对话框中定义了一个图标框和两个文本框,其中的一个文本框中的文字为空,这是以后显示每秒一次的计数值用的。
Timer.asm源程序如下:
;Timer.asm 定时器的使用例子
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff Timer.asm
;rc Timer.rc
;Link /subsystem:windows Timer.obj Timer.res
.386
.model flat,stdcall
option casemap:none ;include 文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib ;equ 等值定义
ID_TIMER1 equ 1
ID_TIMER2 equ 2
ICO_1 equ 1
ICO_2 equ 2
DLG_MAIN equ 1
IDC_SETICON equ 100
IDC_COUNT equ 101 ;数据段
.data?
hInstance dword ?
hWinMain dword ?
dwCount dword ?
idTimer dword ?;代码段
.code
;定时器过程
_ProcTimer proc hWnd, uMsg, idEvent, dwTime pushad invoke GetDlgItemInt, hWinMain, IDC_COUNT, NULL, FALSE inc eax invoke SetDlgItemInt, hWinMain, IDC_COUNT, eax, FALSE popad ret
_ProcTimer endp ;窗口过程
_ProcDlgMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam mov eax, uMsg .if eax == WM_TIMER mov eax, wParam .if eax == ID_TIMER1 inc dwCount mov eax, dwCount and eax, 1 inc eax invoke LoadIcon, hInstance, eax invoke SendDlgItemMessage, hWnd, IDC_SETICON, STM_SETIMAGE, IMAGE_ICON, eax .elseif eax == ID_TIMER2 invoke MessageBeep, -1.endif .elseif eax == WM_INITDIALOGpush hWnd pop hWinMain invoke SetTimer, hWnd, ID_TIMER1, 250, NULL invoke SetTimer, hWnd, ID_TIMER2, 2000, NULL invoke SetTimer, NULL, NULL, 1000, addr _ProcTimer mov idTimer, eax .elseif eax == WM_CLOSE invoke KillTimer, hWnd, ID_TIMER1 invoke KillTimer, hWnd, ID_TIMER2 invoke KillTimer, NULL, idTimer invoke EndDialog, hWnd, NULL .else mov eax, FALSE ret .endif mov eax, TRUE ret
_ProcDlgMain endp main proc invoke GetModuleHandle, NULL mov hInstance, eax invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL invoke ExitProcess,0
main endp
end main
编译运行:
这个程序的基本结构非常简单,就是一个标准的对话框程序而已,在WM_INITDIALOG中用SetTimer申请了3个定时器,并在WM_CLOSE消息中用KillTimer撤销这3个定时器。
申请一个定时器使用SetTimer函数,函数的使用方法如下:
invoke SetTimer,hWnd,nIDEvent,uElapse,lpTimerFunc
hWnd参数是WM_TIMER消息发往的窗口句柄;nIDEvent参数是一个用户指定的任意整数,用来标识一个程序中的多个定时器;uElapse是时间周期,以ms为单位,这个参数是必须指定的;lpTimerFunc是定时器过程,在下面的内容中有详细介绍。如果定时器建立成功的话,函数的返回值是定时器的标识符。
撤销定时器的函数是KillTimer,该函数的使用方法是:
invoke KillTimer,hWnd,uIDEvent
参数hWnd和uIDEvent就是建立定时器时使用的数值。
使用SetTimer函数的方法有两种,第一种方法是要求Windows将WM_TIMER消息发往指定的窗口过程,这时候lpTimerFunc必须为NULL,如例子中的:
invoke SetTimer,hWnd,ID_TIMER1,250,NULL (例1)
invoke SetTimer,hWnd,ID_TIMER2,2000,NULL
这两个句子设置了两个标识分别为ID_TIMER1和ID_TIMER2的定时器,定时周期分别为250 ms和2 s。在窗口过程收到WM_TIMER消息的时候,wParam中是用SetTimer建立定时器时使用的标识uIDEvent,所以程序可以建立一个分支,通过判断wParam来处理不同的定时器引起的WM_TIMER消息。在例子中,当wParam是ID_TIMER1的时候更换图标框中的图标,是ID_TIMER2的时候用MessageBeep函数来发出一声“嘟”的声音。如果要撤销用这种方法建立的定时器,那么只需要用建立时的hWnd和uIDEvent参数简单地调用KillTimer就可以了。
还有一种使用定时器的方法,那就是要求Windows在时间到的时候调用指定的定时器过程,而不是某个窗口过程,那么只需要指定lpTimerFunc参数,如例子中的:
invoke SetTimer,NULL,NULL,1000,addr _ProcTimer (例2)
这句语句要求系统把定时器消息发送到_ProcTimer定时器过程中去,但是,这时候没有参数用来指定定时器标识,到最后如何用KillTimer撤销这个定时器呢?答案是SetTimer函数会返回一个标识,程序可以保存这个标识并在KillTimer函数中使用。
当然,这种用法中的定时器标识也可以自己指定,但这时候一定要同时指定hWnd,虽然这个hWnd没有实际的用途,如果hWnd为NULL,那么即使指定了定时器标识,这个标识也会被忽略,如:
invoke SetTimer, hWnd, ID_TIMER3, 1000, addr _ProcTimer (例3)
这个语句定义了一个标识为ID_TIMER3、消息发往_ProcTimer子程序的定时器。
定时器过程是如下定义的:
TimerProc proc hwnd,uMsg,idEvent,dwTime
Windows回调定时器过程的时候会有4个参数,uMsg总是WM_TIMER,hwnd和idEvent是例3用法中指定的hWnd和定时器标识,如果是例2的用法,那么hwnd就是NULL,而idEvent就是SetTimer返回的由Windows定义的定时器标识。由于有idEvent参数,所以我们同样可以把多个定时器消息指向同一个定时器过程中,并且根据idEvent参数构建一个分支来处理不同定时器引发的消息。
程序中还可能遇到一种情况:当在SetTimer中指定的定时器标识已经存在会怎样呢?答案是Windows会用新的参数代替老的定时器参数,函数执行以后,这个标识的定时器消息将以新的时间周期发送。
读者可能已经注意到,例子程序的窗口过程中把WM_TIMER的消息的处理代码放在第一个分支上,这是对程序的简单优化,把频繁发生的消息放到前面可以使程序少执行一系列的比较指令,像WM_CREATE和WM_DESTROY等仅发生一次的消息可以放到分支的最后面。
6.2 Windows时间
很多读者看到“定时器”这个词的时候往往就联想到时钟,笔者也曾是如此,但是经过6.1节的介绍后就可以发现,定时器是不能用来构造时钟的,定时器用于时钟程序中只能是用在定时刷新屏幕这个功能上,要得到系统的时间还是要靠别的方法。同样道理,定时器也不能用于判断从上次定时器被触发后已经过去了多少时间。下面将介绍其他一些函数来完成这些功能。
6.2.1 Windows时间的获取和设置
在Win32编程中,常用获取系统时间的函数有2个:
invoke GetLocalTime,lpSystemTime
invoke GetSystemTime,lpSystemTime
它们之间的区别是:GetLocalTime返回当前的时间,GetSystemTime返回当前的格林威治标准时间,这两个函数返回的时间数据包括年、月、日、时、分、秒、毫秒,以及星期,由于数据比较多,所以无法放在eax中返回,应用程序需要预先设置一个SYSTEMTIME结构的缓冲区,并将缓冲区地址lpSystemTime当参数传递给函数,函数会把时间数据返回到这个缓冲区中。
SYSTEMTIME结构的定义如下:
SYSTEMTIME STRUCTwYear WORD ? ;年wMonth WORD ? ;月wDayOfWeek WORD ? ;星期,0=星期日,1=星期一,……wDay WORD ? ;日wHour WORD ? ;时wMinute WORD ? ;分wSecond WORD ? ;秒wMilliseconds WORD ? ;毫秒SYSTEMTIME ENDS
需要注意的是,结构中的字段全部是word类型的,而由于Win32程序中用的往往是dword型变量,所以在使用这些数据之前往往要先把它们转换为dword类型,用movzx指令就可以很方便地完成这个工作,如movzx eax,stSystemTime.wYear将wYear字段扩展到32位后放到eax中。
与获取系统时间的函数相对应,可以用下面的两个函数设置系统时间:
invoke SetLocalTime,lpSystemTime
invoke SetSystemTime,lpSystemTime
同样,SetLocalTime中的参数代表本地时间,SetSystemTime中的参数代表格林威治标准时间,在调用函数之前,要把需要设置的时间放到一个SYSTEMTIME结构中并把结构地址当做参数传递给Windows。
【学习笔记】
;GetDateTime.asm Windows环境下32位汇编语言
;在XP系统环境下运行,使用ml命令进行编译和链接:
;ml /c /coff GetDateTime.asm
;link /subsystem:windows GetDateTime.obj.386
.model flat,stdcall
option casemap:none ;区分大小写
;include文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib;数据段
.data
szCaption byte 'Show DateTime', 0
szText byte 'Hello, World !', 0
szFormat byte '格式化字符串:%08x', 0
szFormatTime byte '%d年%d月%d日 %d:%d:%d', 0 ;32位格式
szBuffer byte 256 dup(?), 0
sysTime SYSTEMTIME <>;代码段
.code
_GetDateTimeProc proc local @szBuf[256]:byte local @dwYear:dwordlocal @dwMonth:dwordlocal @dwDay:dwordpushadinvoke GetLocalTime, addr sysTime;以 年 月 日 时:分:秒 的格式显示movzx eax, sysTime.wYearmov @dwYear, eax movzx eax, sysTime.wMonthmov @dwMonth, eax movzx eax, sysTime.wDaymov @dwDay, eax invoke wsprintf, addr @szBuf, addr szFormatTime, @dwYear, @dwMonth, @dwDay, \sysTime.wHour, sysTime.wMinute, sysTime.wSecondinvoke MessageBox, NULL, addr @szBuf, offset szCaption, MB_OKpopadret
_GetDateTimeProc endp main procinvoke _GetDateTimeProc;invoke MessageBox, NULL, offset szText, offset szCaption, MB_OKinvoke ExitProcess, 0
main endp
end main
编译运行结果:
6.2.2 计算时间间隔
在实际的编程中,经常要计算距离上次的时间点已经过去了多少时间,当然,这个数据可以通过两次调用GetLocalTime函数并将两次的时间值相减来得到,唯一的麻烦就是计算的过程比较复杂,因为GetLocalTime函数返回的数据中有年、月、日、时、分、秒和毫秒等数据,将两个时间相减要涉及借位的问题,似乎唯一合理的算法就是首先将这些数据合并成公元以来的总毫秒值再进行相减。
使用时间戳函数GetTickCount可以方便地完成这个功能,GetTickCount函数返回的是Windows本次启动以来的ms数,得到的时间数值直接在eax中返回,这是一个32位的整数,可以表示的范围是1~0ffffffffh ms,所以当Windows连续运行49.7天以后,计数器会清零并重新开始。虽然从这个函数得到的计数值无法用来判断当前的具体时间,但是用来计算两个时间点之间的间隔是最方便不过的了,例如:
invoke GetTickCount
mov dwTickCount1,eax...
invoke GetTickCount
sub eax,dwTickCount1 ;现在eax中就是时间间隔的毫秒数
在Windows 9x系统下,GetTickCount函数的精度为55ms,任何两次调用GetTickCount函数后相减得到的时间间隔要么是0,要么是55ms的整数倍。在Windows NT/2000/XP系统下,函数的精度是10ms。
如果需要得到更精确的时间间隔值,那该怎么办呢?那就要用到Windows的高精度时间戳函数了。
Windows在内部维护一个高精度的计时器,计时的精度取决于计算机的硬件速度,用QueryPerformanceFrequency函数可以获取该计时器每秒钟的计数值:
.data?
dqFreq dq ?
.code
invoke QueryPerformanceFrequency,addr dqFreq
由于计数值比较大,一个32位的整数无法容纳,所以该函数的参数指向一个qword,函数运行后在该qword中返回一个64位的计数值,根据该计数值就可以算出计时器的精度为1000000/dqFreq微秒(μs)。当CPU主频比较高的时候,计数值会比较大,意味着计时器的精度比较高。
得到精度值后,可以将该值保存下来,以便用来计算时间间隔。
QueryPerformanceCounter函数可用来获取高精度计时器的计数值,该函数也是将64位的计数值返回到一个qword中。当两次调用函数得到的计数值是X2和X1,而每秒计数值为Y时,时间间隔就是(X2-X1)×1000000/Y微秒(μs)。鉴于64位的除法在算法上比较复杂,在一般情况下可以用浮点指令来完成计算(对浮点指令不是很熟悉的读者可以参考2.5.2节中提及的《Intel Architecture Software Developer's Manual》或者其他相关书籍):
【学习笔记】
;QueryPerformanceCounter.asm Windows环境下32位汇编语言
;在XP系统环境下运行,使用ml命令进行编译和链接:
;ml /c /coff QueryPerformanceCounter.asm
;link /subsystem:windows QueryPerformanceCounter.obj.386
.model flat,stdcall
option casemap:none ;区分大小写
;include文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib;数据段
.data
szCaption byte 'Show QueryPerformanceCounter', 0
szText byte 'Hello, World !', 0
szFormat byte '运行时间:%ld 微秒', 0
szBuffer byte 256 dup(?), 0dqTickCounter1 dq ? ;时间点1的计数值
dqTickCounter2 dq ? ;时间点2的计数值
dqFreq qword ? ;计数精度
dqTime qword ? ;时间间隔
dw1m dword 1000000 ;常数;代码段
.code
main procinvoke QueryPerformanceCounter, addr dqTickCounter1 ;时间点1;创建无用的计算循环mov ecx, 10000100h
L1:imul ebx imul ebximul ebx loop L1invoke QueryPerformanceCounter, addr dqTickCounter2 ;时间点2invoke QueryPerformanceFrequency,addr dqFreqmov eax, dword ptr dqTickCounter1mov edx, dword ptr dqTickCounter1+4sub dword ptr dqTickCounter2, eaxsbb dword ptr dqTickCounter2+4, edxfinitfild dqFreqfild dqTickCounter2fimul dw1m ;乘以1000000 fdivrfistp dqTime ;dqTime中的64位值就是时间间隔(以微秒为单位) invoke wsprintf, addr szBuffer, addr szFormat, dqTimeinvoke MessageBox, NULL, offset szBuffer, offset szCaption, MB_OK;invoke MessageBox, NULL, offset szText, offset szCaption, MB_OKinvoke ExitProcess, 0
main endp
end main
编译运行结果: