[学习心得二]Win32汇编模块化程序设计的代码组织 By hslwq(于2007-9-22发表)

    在学习与调试罗云彬的〈windows环境下汇编语言程序〉第七章的简易时钟程序时,觉得书中例程把所有的函数放在一个主文件里,阅读与调试很不方便,这种代码组织形式不大符合模块化程序设计思想。为此,我边学习边思考:如何重新组织并优化这个程序代码,使得调试与维护程序时更为方便?
    仔细分析了程序结构后,觉得应该把显示时钟的代码作为一个模块整体,这样在编写与调试显示时钟的代码时,不会影响到主程序框架,从而在编写代码时,可以集中精力考虑如何实现时钟的显示而不必考虑主程序框架,并且这样做有利于代码重用。
    问题是没有人可以告诉我多模块程序该如何组织!
    最近不能上网,也没有汇编的经验,只对C语言编程有点经验,可谓完全的自学,一切只能靠自已的经验或实践探索,故经常会用C来类比思考汇编的问题,一些想法或许可笑,但发笑之余亦有探索的乐趣。
    对汇编模块化程序设计如何组织代码这个问题,我自已探索出了几个可行的方案(所有的方案均以MASMPlus为编辑器,并建立相应的工程):
    方案一:我首先思考是否可以类似C语言的风格组织汇编模块代码。于是作如下实验:
          1、以clock.asm为主文件建立工程,相应头文件为clock.inc。
          2、实现显示时钟的代码组织为头文件showtime.inc与函数实现文件showtime.asm,用include只把showtime.inc包含进clock.inc中,编译失败,找不到调用函数_showtime,把实现文件showtime.asm包含进clock.inc,编译链接成功。去掉语句“include showtime.inc”与"showtime.icn"文件,编译链接成功。
    结论:可以象C语言那样把实现函数实现单独写在一个文件,调用时用include语句包含进主程序。可以不必有头文件的说明,关键是调用函数之前函数实现代码必须包含进调用模块。函数实现代码要以.code开头。这种形式相当于把功能代码移出主程序,使之更具可读性。但与放在主程序的效果是完全一样的。
    方案二:showtime模块单独编译为obj文件,然后与工程一起链接。过程如下:
        1、以clock.asm为主文件建立工程,相应头文件为clock.inc
        2、实现显示时钟的代码组织为头文件showtime.inc,模块函数实现文件组织为showtime.asm
        3、在MASMPuls中把showtime.asm设为单独编译,并在“.obj和工程一起链接”前打勾。
        4、用include只把showtime.inc包含进clock.inc中即可。
    结论:
        1、这种方式因为要把showtime.asm文件单独编译为.obj文件,故在showtime.asm中必须含有指令集说明语句,编译模式,函数调用模式,包含文件等信息,并且结尾要用end结束。
        2、定义的结构应整理在showtime.inc,并包含进showtime.asm文件中。
    方案三:把showtime模块编译为动态链接库供主程序调用。代码组织示例详见源文件。其中showtime.asm要符合动态链接库格式,并在文件参数设置中设置为“单独编译”与“单独链接”,编译参数为:“/c /coff”,链接参数为:“/subsystem:windows /dll /def:showtime.def showtime.obj”,导出_showtime函数供主程序调用,调用前要用include与includelib语句把showtime.inc与showtime.lib包含进主程序头文件中。
    结论:在这种方案中,只有导出的函数_showtime可被主程序调用,但其如何工作是透明的,其它函数对主程序不可见。这样的代码具有良好的封装性,符合模块化程序设计的思想。
以上三种方案中,方案一与方案二编译出来的EXE文件完全相同,方案三则较灵活。
我想把showtime模块生成lib库文件,通过把lib与相应inc文件包含进主程序应该也可以。只是不知道如何把模块编译成lib文件
    值得一提的是showtime模块的编写与组织(详见源代码),其中方案三的showtime.asm文件内容如下:
.386
.model flat,stdcall
option casemap:none
 
include windows.inc
include user32.inc
include kernel32.inc
include gdi32.inc
 
includelib user32.lib
includelib kernel32.lib
includelib gdi32.lib
 
;时钟结构,用于描述时钟的中心与半径
CLOCKSTRUCT        STRUCT
stCenPoint        POINT        <>
dwRadius        DWORD        ?
CLOCKSTRUCT ENDS
LPCLOCKSTRUCT typedef        ptr        CLOCKSTRUCT
 
LPPOINT                        typedef        ptr POINT
 
;角度,描述时针,分针,秒针走过的角度
TIMETODEGREESTRUCT        STRUCT
dwHourDegree        dd        ?
dwMinuteDegree        dd        ?
dwSecondDegree        dd        ?
TIMETODEGREESTRUCT        ENDS
LPTIMETODEGREESTRUCT        typedef ptr TIMETODEGREESTRUCT
 
 
.data?
 
hInstDll        DWORD        ?        ;动态链接库句柄,在本程序中没有用到
 
.code
;动态链接库入口函数,供操作系统加载DLL文件时调用
DllShowTime  proc        _hInstance,_dwReason,_dwReserved
mov        eax,_dwReason
.if        eax == DLL_PROCESS_ATTACH
push        _hInstance
pop        hInstDll
mov        eax,TRUE
.endif
ret
DllShowTime        endp
 
;初始化时钟参数,功能:根据_hWnd获得窗口大小计算时钟中心与半径,并填入stClockParam结构
_InitClockParam        proc _hWnd:HWND,_lpstClockParam:LPCLOCKSTRUCT
local        @stRect:RECT
pushad
invoke        GetClientRect,_hWnd,addr @stRect
mov        eax,_lpstClockParam
mov        ebx,@stRect.right
sub        ebx,@stRect.left
shr        ebx,1
mov        DWORD ptr [eax],ebx
 
mov        ecx,@stRect.bottom
sub        ecx,@stRect.top
shr        ecx,1
mov        DWORD ptr [eax+04h],ecx
 
.if        ecx>ebx
mov        DWORD ptr        [eax+08h],ebx
.else
mov        DWORD ptr        [eax+08h],ecx
.endif
popad
ret
_InitClockParam        endp
 
;系统时间转化为时分秒走过的角度,并填入stTimeToDegree结构
_TimeToDegree        proc        _lpstTimeToDegree:LPTIMETODEGREESTRUCT
local @stTime:SYSTEMTIME
pushad
invoke        GetLocalTime,addr @stTime        ;获得系统时间
 
;计算时针走过的角度
movzx                eax,@stTime.wHour
.if        eax>=12
sub        eax,12
.endif
mov        ecx,360/12
mul        ecx
mov        ebx,eax
movzx        eax,@stTime.wMinute
shr        eax,2
add        ebx,eax
movzx        eax,@stTime.wSecond
mov        ecx,1/120
mul        ecx
add        eax,ebx
mov        ebx,_lpstTimeToDegree
mov        DWORD ptr [ebx],eax
 
;计算分针走过的角度
movzx        eax,@stTime.wMinute
mov        ecx,6
mul        ecx
mov        ebx,eax
movzx        eax,@stTime.wSecond
mov        ecx,1/10
mul        ecx
add        eax,ebx
mov        ebx,_lpstTimeToDegree
mov        DWORD ptr [ebx+04h],eax
 
;计算秒针走过的角度
movzx        eax,@stTime.wSecond
mov        ecx,360/60
mul        ecx
mov        ebx,_lpstTimeToDegree
mov        DWORD ptr [ebx+08h],eax
 
popad
ret
_TimeToDegree        endp
 
_dwPara180        dw        180
 
;根据时钟参数,角度,半径修正参数计算点的坐标
_GetCalcXY        proc        _lpstClockParam:LPCLOCKSTRUCT,_dwDegree:DWORD,_lpLocalPoint:LPPOINT,_dwRadiusAdjust:DWORD
local        @dwDegreeTemp:DWORD
local        @radiusTemp:DWORD
pushad
mov        eax,_lpstClockParam
mov        ebx,_lpLocalPoint
 
;计算横坐标=圆心X+(r-_dwRadiusAdjust)*sin(_dwDegree*pi/180)
fild        DWORD ptr [eax] ;圆心X入栈
fild        _dwDegree       ;角度入栈
fldpi                       ;pi入栈
fmul                        ;弹出st与st(1),计算st*st(1)并压入栈顶,此时st=_dwDegree*pi,st(1)=圆心X
fild        _dwPara180      ;180入栈
fdivp        st(1),st       ;计算st(1)=st(1)/st,并把st出栈,结果st=_dwDegree*pi/180,st(1)=圆心X
fst        @dwDegreeTemp    ;保存_dwDegree*pi/180至@dwDegreeTemp
fsin                        ;sin(_dwDegree*pi/180)
fild        DWORD ptr [eax+08h]    ;半径r入栈
fild        _dwRadiusAdjust        ;_dwRadiusAdjust入栈
fsubp        st(1),st              ;st=r-_dwRadiusAdjust
fst        @radiusTemp             ;保存r-_dwRadiusAdjust至@radiusTemp
fmul                        ;使得st=(r-_dwRadiusAdjust)*sin(_dwDegree*pi/180),st(1)=圆心X
fadd                        ;Localx=圆心X+(r-_dwRadiusAdjust)*sin(_dwDegree*pi/180)
fistp        DWORD ptr [ebx]       ;_lpLocalPoint.x=Localx
 
;计算纵坐标=圆心y-(r-_dwRadiusAdjust)*sin(_dwDegree*pi/180)
fild        DWORD ptr [eax+4]      ;圆心y入栈
fld        @dwDegreeTemp           ;角度_dwDegree*pi/180入栈
fcos                               ;cos(_dwDegree*pi/180)
fld        @radiusTemp             ;r-_dwRadiusAdjust入栈
fmul                               ;使得st=(r-_dwRadiusAdjust)*sin(_dwDegree*pi/180),st(1)=圆心y
fsubp        st(1),st              ;Localy=圆心y-(r-_dwRadiusAdjust)*sin(_dwDegree*pi/180)
fistp        DWORD ptr [ebx+4]     ;_lpLocalPoint.y=Localy
popad
ret
_GetCalcXY        endp
 
;画点,功能:以_dwDegreeInc为步进参数,在_lpstClockParam描述的时钟上画半径为_DwRadius的圆点
_drawDot                proc        _hdc:HDC,_lpstClockParam:LPCLOCKSTRUCT,_dwDegreeInc:DWORD,_DwRadius:DWORD
local        @centerPoint:POINT        ;小圆点圆心
local        @dwNowDegree:DWORD        ;当前圆点角度位置
pushad
mov        @dwNowDegree,0                ;0度开始
.while        @dwNowDegree<=360
 
;画圆
invoke        _GetCalcXY,_lpstClockParam,@dwNowDegree,addr @centerPoint,_DwRadius
mov        eax,@centerPoint.x
sub        eax,_DwRadius
mov        ebx,@centerPoint.y
sub        ebx,_DwRadius
mov        ecx,@centerPoint.x
add        ecx,_DwRadius
mov        edx,@centerPoint.y
add        edx,_DwRadius
invoke        Ellipse,_hdc,eax,ebx,ecx,edx
;步进
mov        eax,_dwDegreeInc
add        @dwNowDegree,eax
.endw
popad
ret
_drawDot                endp
 
;根据_dwDegree画长为(时钟半径-_dwRadiusParam)的线
_drawLine        proc        _hDC:HDC,_lpstClockParam:LPCLOCKSTRUCT,_dwDegree:DWORD,_dwRadiusParam:DWORD
local        @nowPoint:POINT
pushad
mov        eax,_lpstClockParam
mov        ebx,[eax]
mov        ecx,[eax+4]
invoke        MoveToEx,_hDC,ebx,ecx,NULL
 
invoke        _GetCalcXY,_lpstClockParam,_dwDegree,addr @nowPoint,_dwRadiusParam
invoke        LineTo,_hDC,@nowPoint.x,@nowPoint.y
popad
ret
_drawLine        endp
 
;显示时钟
_showTime        proc        _hWnd:HWND,_hDC:HDC
 
local        @stClock:CLOCKSTRUCT                ;时钟参数
local        @stdegreeTime:TIMETODEGREESTRUCT        ;时、分、秒对应角度
 
pushad
invoke        _InitClockParam,_hWnd,addr @stClock        ;根据_hWnd窗口设置时钟参数
invoke        _TimeToDegree,addr        @stdegreeTime        ;当前时间转化为角度
 
invoke        GetStockObject,BLACK_BRUSH
invoke        SelectObject,_hDC,eax
invoke        _drawDot,_hDC,addr @stClock,360/12,3        ;画12大点
invoke        _drawDot,_hDC,addr @stClock,360/60,1        ;画60小点
 
;画时针
invoke        CreatePen,PS_SOLID,3,0
invoke        SelectObject,_hDC,eax
invoke         DeleteObject,eax
invoke        _drawLine,_hDC,addr @stClock,@stdegreeTime.dwHourDegree,35
 
;画分针
invoke        CreatePen,PS_SOLID,2,0
invoke        SelectObject,_hDC,eax
invoke         DeleteObject,eax
invoke        _drawLine,_hDC,addr        @stClock,@stdegreeTime.dwMinuteDegree,20
 
;画秒针
invoke        CreatePen,PS_SOLID,1,0
invoke        SelectObject,_hDC,eax
invoke         DeleteObject,eax
invoke        _drawLine,_hDC,addr        @stClock,@stdegreeTime.dwSecondDegree,10
popad
ret
_showTime        endp
 
end        DllShowTime
说明:
1、定义了两个结构:
CLOCKSTRUCT        STRUCT
stCenPoint        POINT        <>
dwRadius        DWORD        ?
CLOCKSTRUCT ENDS
LPCLOCKSTRUCT typedef        ptr        CLOCKSTRUCT
 
LPPOINT                        typedef        ptr POINT
 
TIMETODEGREESTRUCT        STRUCT
dwHourDegree        dd        ?
dwMinuteDegree        dd        ?
dwSecondDegree        dd        ?
TIMETODEGREESTRUCT        ENDS
LPTIMETODEGREESTRUCT        typedef ptr TIMETODEGREESTRUCT
其中,结构CLOCKSTRUCT用于描述时钟中心与半径,结构TIMETODEGREESTRUCT用于描述当前时针、分针、秒针相对于起始位置转过的角度。
2、定义了如下的函数:
_InitClockParam用于初始化时钟参数,即设置时钟中心与半径。
_TimeToDegree用于把当前时间转化为时针、分针、秒针相对于起始位置转过的角度
_GetCalcXY用于根据时钟参数,角度,半径参考参数计算点的坐标。
_drawDot用于画钟表刻度。
_drawLine用于根据角度,半径参考参数进行画线。
_showTime用于显示时钟。
与〈windows环境下汇编语言程序〉书中程序相比,showtime模块更符合模块化要求:。原因
1、把描述同一对象的变量归结为结构体,在编程中可以减少变量的个数与子程序的参数个数。
2、每个函数的封装性都较好,函数功能清晰明了,没有设置多余的全局变量,使得_showTime函数结构清晰易读,调试维护时也可以快速定位。
3、不同的功能模块组织在不同的文件,方便修改与阅读。

下载...

并不是所有的贴子都是原创,此时作者均指发表的人而不是文章的作者,作者会说明是否是转贴