CHAPTER 5
Procedures
5. 程 序
5.1 导
5.2 结外部函 库
5.2.1 背景知
5.2.2 自我评
5.3 本书所用的 结函 库
5.3.1 概观
5.3.2 个别程序的描述
5.3.2.1 Irvine32.inc标头档
5.3.3 函 库测试程式
5.3.4 自我评
5.4 堆叠运算
5.4.1 执 时间堆叠
5.4.1.1 Push的运作方式
5.4.1.2 Pop的运作方式
5.4.1.3 堆叠的应用
5.4.2 PUSH及POP指
5.4.2.1 PUSH指
5.4.2.2 POP指
5.4.2.3 PUSHFD及POPFD指
5.4.2.4 PUSHAD,PUSHA,POPAD及POPA
5.4.2.5 范 :反转一个字
5.4.3 自我评
5-2 Intel 组合语言
5.5 定义及使用程序
5.5.1 PROC指引
5.5.1.1 定义一个程序
5.5.1.2 范 :三个整 的和
5.5.1.3 程序的注解 明
5.5.2 CALL及RET指
5.5.2.1 呼叫及回传的使用范
5.5.2.2 巢 程序呼叫
5.5.2.3 区域标签及全域标签
5.5.2.4 传递暂存器引 到程序中
5.5.3 范 :计算整 阵 的和
5.5.4 程图
5.5.5 储存及重置暂存器
5.5.5.1 USES运算子
5.5.6 自我评
5.6 使用程序的程式设计
5.6.1 设计整 加法的程式
5.6.1.1 整 加法程式实作
5.6.2 自我评
5.7 本章摘要
5.8 程式设计 习
第5章 程序 5-3
5.1 导
5.1 Introduction
有几个你应该好好地 完这一个章节的好 由:
你需要学习如何在组合语言的环境之中作输入及输出的动作
你需要学习关於执 时期堆叠(Runtime Stack),以及它如何让我们能够
呼叫函 (function)(我们称之为程序(Procedure))的运作原 .
你的程式将可能愈 愈大,你将会需要将它们做 辑上的分割,将整个程
式变成由一段一段的程序组合而成.
程图(Flowcharts)是用 图形表示出程式的 辑结构的工具.在本章节
中,你将学到如何绘制 程图.
如果你是学生,而本书是你的课堂用书;那麼你的教授很可能会考你这一
章的内容.
5-4 Intel 组合语言
5.2 结外部函 库
5.2 Linking to an External Library
如果你愿意花时间,你可以学会如何写出所有输入和输出的细节程式码,
甚至 最基本的输出入动作也可以办到.那就好像当你每次要使用你的 子的
时候,甚至於自己 可以重新组装 子的引擎一样!那的确是件很有趣的事,
但是实在太花时间 !在本书的后段,第11章的地方,你将会有机会学到如何
在MS-Windows保护模式下进 输入及输出的动作.那将是一个非常地有趣的
过程,尤其当你发现到有那麼多种的工具可以用时,等於开启 一个全新的,
完全 同以往的视野.
目前为止,对刚开始接触组合语言的你而言,输入输出应该是相当地简单
的.在本章的第一个部份将会教导你如何由本书所提供的函 库Irvine32.lib中呼
叫一个程序.本书所提供的函 库的完整程式码可以在书后所附的CD中找到,
在本书的专属网站中也将定期地提供必要的 新.其网址如下:
http://www.nuvisionmiami.com/books/asm/index.html
如果你要写的是在真实位址模式下的16位元程式,在本书中也有附上和
Irvine32.lib内含相同程序的16位元函 库Irvine16.lib.
第5章 程序 5-5
5.2.1 背景知
结函 库(Link Library)是一个包含多个已经被组译成机器码程序的档
案.这些在函 库 的程式码一开始本 是包含 程序,常 及变 的程式码
的原始档.原始档再被组译成目的档,然后再被收集到一个函 库中.
假如你想让你的程式显示出一段字 到萤幕上,你将需要呼叫名为
Writestring的程序.你的程式 将需要包含一个PROTO的指引(directive, 或译
为假指 , 3.1.8), 命名这个要呼叫的程序.以下的指引可以在名为Irvine32.inc
的档案中找到:
WriteString PROTO
接下 , 用CALL指 执 Writestring程序:
call Writestring
当你的程式被组译时,组译器会空下一段记忆体位置给CALL指 ,因为组译
器知道将 这段位置会被 结器填入资 . 结器会在 结函 库中寻找名为
Writestring的程序,并且会将其机器码指 复制到你程式的执 档之中.并且,它还
会将Writestring的位置插入到CALL指 之中填入原先空下 的位置上.
如果你试著要呼叫一个 在 结函 库中的程序, 结器将会显示错误讯
息,并且将 会产生你的程式的执 档.
结器功能选项 (Linker Command Options)
Link 结器可以将你的程式的目的档和一个或多个目的档及函 库做
结.下 的指 是一个范 ,它将hello.obj与irvine32.lib及kernel32.lib函 库
结:
link32 hello.obj irvine32.lib kernel32.lib
先前在书中用 组译并 结的批次档(make32.bat或make16.bat)也是使用
几乎一样的指 .唯一 同的地方是在一个可置换的 (%1),被用 取代
「hello.obj」.这样子做可以让这个批次档用 结任何程式:
link32 %1.obj irvine32.lib kernel32.lib
5-6 Intel 组合语言
整体结构 (Overall Structure)
你也许会对kernel32.lib这个档案在图中的位置感到 解.这个档案是由
Microsoft Windows平台中的Software Development Kit(一般人简称SDK)所提供
的.它包含 用 和作业系统 结的相关资讯,而这些资讯存放在另一个名为
kernel32.dll的档案中.这个档案是MS-Windows作业系统的基本要件,它的名
称叫动态 结函 库(Dynamic Link Library).它包含 许多用 做字元层面
的输入输出的可执 档.你或许可以把kernel32.lib想成是用 和kernel32.dll
沟通的一座桥梁,正如下图所示:
使用你在这章中所学到的东西,你的程式将可 结到Irvine32.lib函 库.
在之后的第11章中,你将会学到如何将你的程式直接地 结到kernel32.lib函
库.
Irvine32.lib你的程式
kernel32.lib
结到
结到
也可 结到
执
kernel32.dll
第5章 程序 5-7
5.2.2 自我评
1. (是非题): 结函 库(link library)中包含 结合语言的原始程式码.
2. 使用PROTO指引(directive, 指引) ,宣告一个存在於外部 结函 库中名为
MyProc的程序.
3. 写一段用CALL指 的程式,呼叫位於一个外部 结函 库中,名为MyPorc
的程序.
4. 书中所提供的三十二位元的 结函 库叫 麼名字
5. 那一个函 库包含由Ivine32.lib函 库呼叫的函 .
6. kernel32.dll是 麼
7. 在批次档make32.bat之中,用 取代档案名称的 叫 麼名字
5-8 Intel 组合语言
5.3 本书所用的 结函 部
5.3 The Book's Link Library
5.3.1 概 观
表5-1是在Irvine32 结函 库之中所包含的程序速查表(一些其他的程
序将在后面的章节再做介绍).首先,一些名词必需要先在此做个解释:
主控台(console):一个在MS-Windows下执 文字模式的三十二位元的主
控视窗.预设值应该包含80 (columns),25 (rows).
标准输入(standard input):标准的输入装置是键盘.但我们仍然可以在命
模式下将标准输入重新导向为任何一个档案或序 埠, 做为标准输入的
源.
标准输出(standard output):标准输出装置是显示器.但我们仍然可以在命
模式下将标准输出重新导向为写入一个档案,印表机或是序 埠.
表5-1 结函 库中的程序
程 序 明
Clrscr 清除主控台,并将游标重新定位於左上角.
Crlf 写入一个 尾标记(end-of-line)到标准输出.
Delay 暂停程式执 n个10-3秒(millisecond, 毫秒)的间隔.
DumpMem
以十 进位的表示方式,将一个区段的记忆体内容写入到标准
输出.
DumpRegs
以十 进位的方式显示EAX,EBX,ECX,EDX,ESI,EDI,
EBP,ESP,EFLAGS及EIP暂存器;同时也显示进位,符号,
值及溢位旗标.
GetCommandtail
复制程式命 的 (也称为command tail)到一个位元组
阵 .
GetMseconds
以毫秒(milliseconds)为单位,回传一个代表过 午夜十二
点多久时间的 值.
第5章 程序 5-9
Gotoxy 在主控视窗中的游标定位到在指定的 .
Random32
产生一个32-bit的假随机(pseudorandom)整 , 值范围在
由0到FFFFFFFF之间.
Randomize 用於产生 种子.
RandomRange 产生一个在一定 值范围内的假随机整 .
ReadChar 由标准输入 入一个单独的字元.
ReadHex
由标准输入( 如Keyboard) 取一个32-bit的十 位元整
,并在接收到[Enter]键时结束 取动作.
ReadInt
由标准输入介面 取一个32-bit的有号十进位整 ,并在接收
到[Enter ]键时结束 取动作.
ReadString
由标准输入 取一个字 ,并在接收到[Enter]键时结束 取动
作.
SetTextColor
设定所有在主控视窗中的文字输出结果的前景及背景的颜
色.(此功能在Irvinel16.lib中并没有提供)
WaitMsg 显示一个讯息,并等待输入键被按下.
WriteBin 以ASCII二进元格式写入一个无号的32-bit整 到标准输出.
WriteChar 写入一个单独的字元到标准输出.
WriteDec 以十进位格式写入一个无号的32-bit整 到标准输出.
WriteHex 以十 进位格式写入一个无号的32-bit整 到标准输出.
WriteInt 以十进位格式写入一个有号的32-bit整 到标准输出.
WriteString 写入一个以Null结尾的字 到标准输出.
5-10 Intel 组合语言
5.3.2 个别程序描述
Clrscr
这个程序是用 清除萤幕用的.基本上会使用在开启及结束一个程式的时
候.假设你想在程式执 过程的其他时机呼叫这个程序,记得在呼叫它之前先
暂停程式的执 态(可以藉由呼叫WaitMsg程序),这可以让使用者在清掉
萤幕前先看完萤幕上所显示的资讯.叫用范 :
call Clrscr
Crlf
这个程序会将标准输出的游标移至下一 的开头.它藉由写入一个包含二
个位元组的字 ,0Dh及0Ah, 达到这样子的效果.叫用范 :
call Crlf
Delay
这个程序被呼叫时会暂停程式的执 态一段特定的时间间隔.当呼叫
时,将EAX以10-3秒(millisecond)为单位,设定你想要暂停的时间间隔.叫用范
(在Irvine16.lib版本中的这个程序无法在Windows NT,2000或XP中执 ):
mov eax,1000 ; 1 秒
call Delay
DumpMem
这个程序会将一段范围内的记忆体内容,以十 进位的格式写入到标准输
出上.当你叫用这个程序时,将启始位置的值传入ESI,单位 值传到ECX
及单位大小值传入EBX(1 = byte,2 = word,4 = doubleword).以下的叫用范
,会显示名为array的阵 ,它包含 11个双字组:
.data
array DWORD 1, 2, 3, 4, 5, 6, 7, 8, 9, 0Ah, 0Bh
.code
main PROC
mov esi, OFFSET array ; 启始偏移值
mov ecx, LENGTHOF array ; 元素个
mov ebx, TYPE array ; doubleword 格式
call DumpMem
第5章 程序 5-11
下 的输出是由上述的范 中的DumpMem所产生的:
00000001 00000002 00000003 00000004 00000005 00000006
00000007 00000008 0000009 0000000A 0000000B
DumpRegs
这个程序会以十 进位格式显示EAX,EBX,ECX,EDX,ESI,EDI,EBP,
ESP,EIP及EFL(EFLAGS)暂存器的值.它同时也显示出进位,符号, 值
及溢位旗标.以下是它显示的范 :
EAX=00000613 EBX=00000000 ECX=000000FF ECX=00000000
ESI=00000000 EDI=00000100 EBP=0000091E ESP=000000F6
EIP=00401026 EFL=00000286 CF=0 SF=1 ZF=0 0F=0
其中EIP所显示的值,是在DumpRegs被呼叫后的下一个指 的偏移值.当
在为程式除错时,DumpRegs是很有用的,因为它可以让你清楚地了解到程式执
中的某一刻CPU的 态.这个程序并 需要输入任何 ,也没有回传值.
( GetCommandtail
这个程序会将程式的命 复制到一个以Null结尾的字 .当命 是空
的时候,进位旗标会被设定,否则,进位旗标会被清除.这个程序当在需要使
用者在程式进 中输入资 时非常好用.
如,假设一个名为Encrypt的程式能够 取一个输入档,档名为file1.txt,
并且会产生一个输出档,名为file2.txt.使用者可以在使用程式时在命 中输
入这二个档名:
Encrypt file1.txt file2.txt
当程式开始时,Encrypt程式可以呼叫GetCommandtail,用 接收这二个档
案名称.当呼叫GetCommandtail,时EDX必需要包含一个至少129 bytes的阵
的偏移值:
.data
cmdTail BYTE 129 DUP(0) ; empty buffer
.code
mov edx, OFFSET buffer
call GetCommandtail ; 填入buffer
5-12 Intel 组合语言
GetMseconds
这个程序会以毫秒milliseconds为单位回传一个 值,代表自从午夜十二点
过后到现在经过 多少时间.当你需要测 在二个事件发生之间经过 多少时
间时,这个程序会是很好用的.回传值会传到EAX中, 需要输入任何的 .
在以下的范 中,我们将呼叫这个程序一次,并储存它的回传值.接下 ,一
个回圈被执 ,最后,我们再一次呼叫GetMseconds程序,然后把得到的二个
时间值相减.我们 得出 确的回圈执 时间,而且是以毫秒为单位.
.data
startTime DWORD
.code
call GetMseconds
mov startTime,eax
L1:
; (在这 执 回圈)
Loop L1
Call GetMseconds
Sub eax,startTime ; EAX = 以毫秒为单位的回圈执 时
间
Gotoxy
这个程序可藉由输入特定的 (row)与 (column)的值 重新定位游标
在主控视窗中的位置.在Windows的主控视窗中,预设的X轴的值(即 或视
窗寛 的值)为0到79,而Y轴的值(即 或视窗高 的值)为0到24.当你
呼叫Gotoxy,你需要将你想要的Y轴的值传入DH中,而X轴的值传入DL中.
叫用范 :
mov dh, 10 ; 10
mov dl, 20 ; 20
call Gotoxy ; 定位游标位置
Random32
这个程序会随机产生一个32-bit的整 并回传其值到EAX中.当此程
序被重复呼叫时,它会产生一组虚拟 序 (Simulated Random Sequence),
其中包含的整 称为假 整 (Pseudorandom Integer)(注:如果你想多 解
第5章 程序 5-13
产生器,请 阅Donald Knuth所著的The Art of Computer Programmming (Vol.2),
Addison-Wesley,1997).这些整 由一个简单的函 所产生,它需要一个输入值称为
种子(Seed).该函 会 用 种子在一个方程式中产生第一个 值,
然后接下 的随机值会以前一个 值当成 种子 产生.一般而言,我们
使用 (Random)这个名词 代表假 (Pseudorandom).叫用范 如下:
.data
randVal DWORD
.code
call Random32
mov randVal,eax
Randomize
这个程序是用 产生 种子,以供Random32及RandomRange二个程序
之中的 产生函 使用. 种子会相等於叫用的时间, 确 到1/100秒.
这很明显地表示当每次你执 一个程式,你所得到的启始 值 将会 同,
并且每一组 序 也将 相同,各自独 .你只需要在程式开始执 之初呼
叫Randomize程序一次就够 .在下 的范 之中,我们将产生十个 值:
call randomize
mov ecx, 10
L1: call random32
;在此处使用或显示在EAX中的 值
Loop L1
RandomRange
这个程序可以产生一个介於0到(n-1)的范围中的 ,其中,n是一个
,将会被输入到EAX暂存器中,产生出 的 值会被回传到EAX之中.
下 的范 会产生一个单独的 ,介於0到4999,并储存到EAX之中:
.data
randVal DWORD
.code
mov eax, 5000
call RandomRange
mov randVal, eax
5-14 Intel 组合语言
ReadChar
这个程序可以用 由标准输入 取一个单独的字元,并将该字元传到AL暂
存器之中.该字元 会被显示在萤幕上.下 是一段叫用的范 程式码:
.data
char BYTE
.code
call ReadChar
mov char,al
ReadHex
ReadHex可以用 在标准输入中, 取一个32-bit的十 进位整 ,并把该
值存放到EAX之中.它 会检查输入的值是否为有效值,你可以输入大写或小
写的A到F 代表 字.输入的最大 字为八位 ,并且无法输入以空白为启
始的 字.以下为叫用范 :
.data
hexVal DWORD
.code
call ReadHex
mov hexVal, eax
ReadInt
ReadInt可以用 在标准输入中, 取一个32-bit的有号整 ,并把回传值
存到EAX之中.使用者可以在开始的第一个位元中输入正,负号,而在其他的
位元中只能输入 字.ReadInt会设定溢位旗标,并且会在输入值无法以32-bit
有号整 的方式(范围:-2,147,483,648到+2,147,483,647)表示时显示错误讯息.
叫用范 如下:
.data
intVal SDWORD
.code
call ReadInt
mov intVal, eax
第5章 程序 5-15
ReadString
这个程序会由标准输入 取一个字 ,并在使用者按下[Enter]键时结束 取
动作.它会计算 取 多少个位元组,并回传计算结果到EAX暂存器之中.在
叫用ReadString之前,必需先将EDX设为字元要输入的阵 位置的偏移值,作
为储存输入字元的缓冲区,并将ECX定义为可 取字元的最大 值.
以下的程式片段会呼叫ReadString,并传入ECX及EDX.要特别注意的是,
我们将ECX的缓冲区值减去一, 扣掉输入字 的Null结尾.
.data
buffer BYTE 50 DUP(0) ; 定义字 长
byteCount DWORD ; 定义字 长 计算变
.code
mov edx, OFFSET buffer ; 指自缓冲区
mov ecx, (SIZEOF buffer) - 1 ; 指定最大 取字 长
call ReadString ; 输入字
mov byteCount, eax ; 字 的长
ReadString程序会自动地在键入字 的结尾加上一个Null字元.以下在使
用者输入一个"ABCDEFG"的字 后,以十 进位格式以及ASCII Dump的
方式,显示缓冲区中的前八个位元组的:
41 42 43 44 45 56 47 00 ABCDEFG
其变 的总位元组 计算结果(byteConut值)会等於7.
5-16 Intel 组合语言
SetTextColor
顾名思义,这个程序可以让我用 设定文字的颜色和底色,下 的表格是
我们可以用 事先定义的文字颜色和底色的选项:
黑 = 0 红 = 4 灰 = 8 淡红 = 12
= 1 洋红 = 5 淡 = 9 淡洋红 = 13
= 2 棕 = 6 淡 = 10 黄 = 14
青 = 3 淡灰 = 7 淡青 = 11白 = 15
这些颜色常 被定义在Irvine32.inc及Irvine16.inc之中.背景的颜色必需
乘上十 后再加到前景颜色之中(代表其值向左平移四个位元,你将会在第7章 到相
关的内容).下 的常 值,举 而言,表示黄色的字在 色的底色上:
yellow + (bulu * 16)
在呼叫SetTextColor之前,需将所要的颜色常 移到EAX之中:
mov eax, white + (blue * 16) ; 白色字, 底色
call SetTextColor
(假如你想阅 多关於显示色彩的资 ,请查阅本书的15.3.2.SetTextColor无法在Irvine16
结函 库中被使用.)
WaitMsg
这个程序会在萤幕上显示"Press [Enter] to continue …"的讯息,并让程式
停止直到使用者按下输入键时才会继续动作.当你希望程式暂停萤幕显示,让
萤幕上的资 能卷动并显示完全时相当好用.它 需要输入任何 .叫用范
如下:
call WaitMsg
WriteChar
这个程序让你可以写入一个单独的字元到标准输出.在呼叫程序之前,需
先将字元传入AL中(或其ACSII码):
mov al, 'A'
call WriteChar ; 显示:"A"
第5章 程序 5-17
WriteDec
这个程序让你能在标准输出中写入一个32-bit的无号整 ,并会以十进位
的方式显示出 ,而且 能以 做为开头.在叫用它之前,先叫整 传入EAX
之中:
mov eax, 295
call WriteDec ; 显示: "295"
WriteHex
这个程序会写入一个32-bit的无号整 到标准输出中,并以八进位的格式
显示.如果有需要,程序会在开头处补上 .在叫用它之前,先将整 传入EAX
中:
mov eax, 7FFFh
call WriteHex ; 显示: "00007FFF"
WriteInt
这个程序会写入一个32-bit的有号整 到标准输出中,以十进位的格式显
示,并在开头处加上正,负号,但 能以 做为开头.在叫用前,先将整 传
入EAX中:
mov eax, 216543
call WriteInt ; 显示: "+216543"
WriteString
这个程序会写入一个以Null做结尾的字 到标准输出中.当叫用它时,先
将字 的缓冲区设到EDX中.叫用范 :
.data
prompt BYTE "Enter your name:", 0
.code
mov edx, OFFSET prompt
call WriteString
5-18 Intel 组合语言
5.3.2.1 Irvine32.inc标头档
以下 出 部份Irvine32.inc,include档的档案内容,它包含 在函 库中
每个程序的原型,同时包含 颜色常 ,结构及符号的定义.这个档案会时常
会 新,所以为确保你所用的是最新的版本,请至本书的专属网页上 新:
; Include file for Irvine32.lib (Irvine32.inc)
INCLUDE SmallWin.inc
.NOLIST
;----------------------------------------
; Procedure Prototypes
;----------------------------------------
ClrScr PROTO
Crlf PROTO
Delay PROTO
DumpMem PROTO
DumpRegs PROTO
GetCommandtail PROTO
GetMseconds PROTO
Gotoxy PROTO
Randomize PROTO
RandomRange PROTO
Random32 PROTO
ReadInt PROTO
ReadChar PROTO
ReadHex PROTO
ReadString PROTO
SetTextColor PROTO
WaitMsg PROTO
WriteBin PROTO
WriteChar PROTO
WriteDec PROTO
WriteHex PROTO
WriteInt PROTO
WriteString PROTO
第5章 程序 5-19
;-----------------------------------
; Standard 4-bit color definitions
;-----------------------------------
black = 0000b
blue = 0001b
green = 0010b
cyan = 0011b
red = 0100b
magenta = 0101b
brown = 0110b
lightGray = 0111b
gray = 1000b
lightBlue = 1001b
lightGreen = 1010b
lightCyan = 1011b
lightRed = 1100b
lightMagenta = 1101b
yellow = 1110b
white = 1111b
.LIST
在这个档案开头的.NOLIST指引可避免这些内容出现在组译器所产生的清
单档中.而在档案尾端的.LIST指引,则会回 清单档的建 动作.在档案一开
始的INCLUDE指引可使得另一个include档(SmallWin.inc)也被引入到组辑器
中.SmallWin.inc这个档案包含 直接呼叫MS-Windows函 时,所要的函 原
型,常 及资 结构.我们将会在第11章时讨 这些内容.
5-20 Intel 组合语言
5.3.3 函 库测试程式
让我们看一下可以用 测试本书所提供的 结函 库中特定程序的小程
式.在程式中的注解 明 每一个步骤:
TITLE Testing the Link Library (TestLib.asm)
; Testing the Irvine32 Library.
INCLUDE Irvine32.inc
CR = 0Dh ; carriage return
LF = 0Ah ; line feed
.data
str1 BYTE "Generating 20 random integers between "
BYTE "0 and 990: ", CR, LF, 0
str2 BYTE "Enter a 32-bit signed integer: ", 0
str3 BYTE "Enter your name: ", 0
str4 BYTE "The following key was pressed: ", 0
str5 BYTE "Displaying the registers: ", CR, LF, 0
str6 BYTE "Hello, ", 0
buffer BYTE 50 dup(0)
dwordVal DWORD
.code
main PROC
; Set text color to black text on white background:
mov eax, black + (white * 16)
call SetTextColor
call Clrscr ; clear the screen
call Randomize ; reset random number sequence
; Generate 20 random integers between 0 and 990.
; Include a 500 millisecond delay.
mov edx, OFFSET str1 ; display message
call WriteString
mov ecx, 20 ; loop counter
mov dh, 2 ; screen row 2
mov dl, 0 ; screen column 0
第5章 程序 5-21
L1: call Gotoxy
mov eax, 991 ; indicate top of range + 1
call RandomRange ; EAX = random integer
call WriteDec ; display in unsigned decimal
mov eax, 500
call Delay ; pause for 500 milliseconds
inc dh ; next screen row
add dl,2 ; move 2 columns to the right
Loop L1
call Crlf ; new line
call WaitMsg ; "Press [Enter]..."
call Clrscr ; clear screen
; Input a signed decimal integer and redisplay it in
; various formats:
mov edx, OFFSET str2 ; "Enter a 32-bit..."
call WriteString
call ReadInt ; input the integer
mov dwordVal, eax ; save in a variable
call Crlf ; new line
call WriteInt ; display in signed decimal
call Crlf
call WriteHex ; display in hexadecimal
call Crlf
call WriteBin ; display in binary
call Crlf
; Display the CPU registers:
call Crlf
mov edx, OFFSET str5 ; "Displaying the registers:"
call WriteString
call DumpRegs ; display registers and flags
call Crlf
; Display a memory dump:
mov esi, OFFSET dwordVal ; starting OFFSET
mov ecx, LENGTHOF dwordVal ; number of units in dwordVal
mov ebx, TYPE dwordVal ; size of a doubleword
call DumpMem ; display memory
call Crlf ; new line
call WaitMsg ; "Press [Enter]..."
; Ask the user to input their name:
5-22 Intel 组合语言
call Clrscr ; clear screen
mov edx, OFFSET str3 ; "Enter your name:"
call WriteString
mov edx, OFFSET buffer ; point to the buffer
mov ecx, SIZEOF buffer - 1 ; max. number characters
call ReadString ; input the name
mov edx, OFFSET str6 ; "Hello, "
call WriteString
mov edx, OFFSET buffer ; display the name
call WriteString
call Crlf
exit
main ENDP
END main
输出范
以下是程式输出的范 .这些 字是 产生的,因此,你的程式输出的
结果可能会和范 的 同:
第5章 程序 5-23
在你按下输入键之后,程式会显示以下的资 :
当你输入你的名字时,程式会再显示一次(最后一个"Press any key"并
是由程式所产生的):
5-24 Intel 组合语言
5.3.4 自我评
1. 在 结函 库中,那一个程序会在固定范围中产生一个
2. 在 结函 库中,那一个程序会显示"Press [Enter] to continue…"并等待使
用者按下[Enter]键
3. 写一段程式让一个程式能暂停700毫秒.
4. 那一个函 库中的程序可以在标准输出中以十进位格式写入一个无号整
5. 那一个程序能将游标定位在视窗中的特定位置
6. 写出当在使用Irvine32函 库时所需要的INCLUDE的指引.
7. 在Irvine32.inc中是 麼型式的程式码
8. DumpMem程序需要输入 麼
9. ReadString程序需要输入 麼
10. 那些程序 态旗标会在DumpRegs程序被叫用时显示出
11. 挑战:写一段程式码,提示使用者一个特定的 字,并输入一个 字字
到一个位元组阵 之中.
第5章 程序 5-25
1
2
3
4
5
6
7
8
9
10
5.4 堆叠运算
5.4 Stack Operations
当你将十个盘子一个一个往上叠,就如同下图所示的方式时,那 是我们
所谓的堆叠(Stacks).假设每一个盘子 相当地重,那麼可想而知的,当我们
想从这个堆叠中间移出一个盘子,是 可能的事,但是我们可以由堆叠的最上
方移出盘子.同 ,要加入一个盘子到堆叠之中也是一样的,我们只能从堆叠
的上方加入,而绝对 可能从中间加入:
顶层
底层
我们把堆叠称做一个LIFO的结构(Last-in,first-out;后进先出).因为
最后一个被加入堆叠的资 ,在从堆叠取出资 时,会被最先取出(LIFO是众所
皆知的名词,但我还是喜欢用盘子 比喻它,因为盘子会提醒我该吃 ).
所有堆叠资 结构(Stack Data Structure) 遵 相同的原则:加入新资
时由堆叠的最上方加入,而 取资 时也是由最上方移出.一般而这,堆叠是
各种程式写作的应用中很好用的一种结构,它可以很容 地以物件导向的程式
方法被实作出 .假如你曾经上过需要 用资 结构的程式写作课程,你应该
已经知道 麼是堆叠抽象资 型态(Stack Abstract Data Type).
然而,在本章之中,我们只会将注意 放在执 时期堆叠(Runtime Stack)
之上.这是直接由CPU中硬体支援的,而且它是叫用和回传程序的原 中,一
个 可缺部份.大部份的时候我们简称它为堆叠(Stack).
5-26 Intel 组合语言
5.4.1 执 时期堆叠
执 时期堆叠是一段直接由CPU管 的记忆体阵 ,它使用 二个暂存
器:SS和ESP.在保护模式下,SS暂存器会保有一个无法以使用者程式改变的
区段描述器(segment descriptor).ESP暂存器则保有一个在堆叠之中某处的32-bit
徧移值.我们很少会直接改变ESP的值,反而是间接地使用一些指 去修改它,
如:CALL,RET,PUSH及POP.
堆叠指标暂存器(Stack Pointer Register)ESP,会指向最后一个加入堆叠
的资 位置,或是最后一个移出堆叠的资 位置.为 明,让我们以一个只
包含一笔资 的堆叠做为开始.在下 的图示中,延伸堆叠指标(Extended Stack
Pointer, ESP)的内容为最新被加入的一笔资 (00000006)的徧移值,一个十
进位的 00001000.
偏移值
ESP00001000 00000006
00000FFC
00000FF8
00000FF4
00000FF0
在图示中,每一个堆叠中的位置 占32个位元,这是因为这个程式是在保
护模式下执 的原故.在实体位址模式下,每一个位置只占16位元,并且由SP
暂存器指向最新加入的资 位置.
第5章 程序 5-27
5.4.1.1 PUSH运算
32-bit的堆叠PUSH运算会将堆叠指标的值减四,并将资 拷贝到堆叠指
标在堆叠之中所指到的位置.如下图所示,我们将PUSH 000000A5这个值到堆
叠之中:
之前 之后
00000006 00001000 ESP0000100000000006
00000FFC ESP 000000A5 00000FFC
00000FF8 00000FF8
00000FF4 00000FF4
00000FF0 00000FF0
或许你已经注意到 ,上图所画的堆叠方向,和我们在本章一开始时所讲的
堆叠方向刚好相反.并没有任何原因造成执 时间堆叠无法在记忆体之中往上做
堆叠,只是,Intel的工程师在设计这部份的内容时,决定让它往下堆叠. 它
是往上还是往下,只要是堆叠 仍旧遵循后进先出的原则!
在PUSH运算前,ESP = 00001000h,之后,ESP = 00000FFCh.下图表示在
相同的堆叠中,再加入二笔新的资 的结果:
Offset
0000100000000006
000000A500000FFC
0000000100000FF8
00000002ESP 00000FF4
00000FF0
5-28 Intel 组合语言
5.4.1.2 POP运算
POP运算会由堆叠之中移除一笔资 ,并将它放进一个 或暂存器之
中.在资 被从堆叠中移出后,堆叠指标会增加并指向下一笔资 的位置.下
的图示 明 00000002这笔资 被由堆叠中移出的之前及之后,堆叠的 态:
之前 之后
00000006 00001000
00000FFC
00000FF8
00000FF4
00000FF0
ESP
00000006 00001000
000000A5 000000A5 00000FFC
00000FF800000001 ESP 00000001
00000002 00000FF4
00000FF0
在ESP以下的堆叠区段我们称作 辑上空的(Logically Empty),这些资
区在同一个程式下一次执 任何一个会填入堆叠动作的指 时被填入新的资
.
5.4.1.3堆叠的应用
以下有几个堆叠在程式中重要的应用方法:
当你希望暂存器能有超过一个以上的用处时,堆叠能提供一个方 的,暂
时的存放区域.当这些暂存器被修改后,它们仍然能回 成原 的值.
当CALL指 被执 时,CPU会将现 程序的返回位址(return address)存
入堆叠之中.
当呼叫一个程序时,我们常会传递要输入的变 ,称为引 (Arguments).
这些引 将被PUSH到堆叠之中.
在程序中的区域变 会被以堆叠之式产生,并在程序结束后被清除.
第5章 程序 5-29
5.4.2 PUSH及POP指
5.4.2.1 PUSH指
PUSH指 会先减少ESP暂存器的值,然后复制一个16位元或32位元的
源运算元到堆叠之中.如果复制的是16位元的运算元,会让ESP值减去2;
而32位元的运算元,会让ESP值减去4.以下是三种指 格式:
PUSH r/m16
PUSH r/m32
PUSH imm32
假如你的程式是要从Irvine32函 库呼叫一个程序,那麼你应该只PUSH
32-bit的资 :否则,函 库所使用的Win32主控台函 会无法正确地执 .如
果你的程式呼叫的是在Irvine16函 库中的程序(在实体位址模式下),你可以
使用16-bit或是32-bit的资 可以.
保护模式下的即时资 (Immediate Values)永远 是32位元的.在真实
模式之下,即时资 的预设值是16位元的,除非你使用 .386处 器指 集
(.386指 集在章节3.2.3介绍过 .)
5.4.2.2 POP指
POP指 会先复制堆叠之中由ESP暂存器所指向的内容,并将它复制到一
个16位元或32位元的目的运算元之中,然后增加ESP的值.当目的运算元是
16位元时,ESP的值会增加2;32位元时,ESP会增加4:
POP r/m16
POP r/m32
5-30 Intel 组合语言
5.4.2.3 PUSHFD及POPFD指
PUSHFD指 会将32位元的EFLAGS暂存器PUSH到堆叠之中,而POPFD
则是由堆叠之中将资 POP到EFLAGS暂存器中:
pushfd
popfd
真实位址模式(Real-Address Mode)程式以PUSHF指 将16位元的FLAGS
暂存器传到堆叠之中,并以POPF指 由堆叠之中取出资 传到FLAGS之中.
有时候,替旗标做个备份,让你在之后可以恢 它们先前的值,在写程式
时会很好用.其中一个方法就是直接将介於PUSHFD及POPFD之间的每一个区
段的程式码围住:
pushfd ; 储存旗标
;
; 在此处的任何程式陈述片段….
;
popfd ; 原旗标
当在使用这种 型的PUSH及POP时,你一定要小心注意到程式的执 并
会跳过POPFD.当一个程式一再被修改时(或是被 新时),你可能会很难
记得每一个PUSH及POP的位置.一个能够达到相同作用,又比较 出错的
方式是将旗标存到变 之中:
.data
saveFlags DWORD
.code
pushfd ; 将旗标PUSH到堆叠中
pop saveFlags ; 复制到变 之中
下 的程式码会将旗标由变 中,回 成原 的值:
push saveFlags ; PUSH已储存的旗标值
popfd ; 复制到旗标之中
第5章 程序 5-31
5.4.2.4 PUSHAD,PUSHA,POPAD及POPA
PUSHAD指 可以将任何一般用途的32位元暂存器,以下 的顺序,PUSH
到堆叠当中:EAX,ECX,EDX,EBX,ESP(原始值),EBP,ESI及EDI.
POPAD指 会将相同的暂存器由堆叠之中,以相反的顺序POP出 .相同地,
PUSHA指 曾在之前介绍80286处 器时介绍过,会将任何一般用途的16位元
暂存器PUSH进堆叠之中,依照下 的顺序:AX,CX,DX,BX,SP(原始值),
BP,SI,DI.POPA指 则会以相反的顺序,由堆叠中POP出以上这些暂存器.
如果你要写一个程序,用 修改一些32位元暂存器,那麼你应该在这个程
序的一开始就使用PUSHAD,并在结尾处使用POPAD 储存并回 这些暂存
器.以下的程式片段是一个 明范 :
MySub PROC
pushad ; 储存一般用途的暂存器
.
.
mov eax, ...
mov edx, ...
mov ecx, ...
.
.
popad ; 回 一般用途的暂存器
ret
MySub ENDP
5-32 Intel 组合语言
5.4.2.5范 :反转一个字
RevString.asm这个程式以回圈方式接收字 ,并将一个个字元PUSH到堆
叠之中.然后再由堆叠中POP(以相反的顺序)出 ,并将之存回原 的字
变 中.因为堆叠是一个LIFO结构(后进先出),所以字 会以相反的顺序显
示:
TITLE Program Template (RevString.asm)
INCLUDE Irvine32.inc
.data
aName BYTE "Abraham Lincoln", 0
nameSize = ($ - aName) - 1
.code
main PROC
; 将姓名PUSH到堆叠中.
mov ecx, nameSize
mov esi, 0
L1: movzx eax, aName[esi] ; 取字元
push eax ; PUSH到堆叠
inc esi
Loop L1
; 以相反顺序由堆叠中POP出姓名,
; 将POP出 的资 存到aName阵 中.
mov ecx, nameSize
mov esi,0
L2: pop eax ; 取字元
mov aName[esi], al ; 存到字
inc esi
Loop L2
; 显示姓名资 .
mov edx, OFFSET aName
call Writestring
call Crlf
exit
main ENDP
END main
第5章 程序 5-33
5.4.3 自我评
1. 那二种暂存器(在保护模式下)用 维护堆叠的存放
2. 执 时间堆叠和堆叠抽象资 型别有那些 同
3. 为何堆叠会被称作为LIFO结构
4. 当一个32位元的值被PUSH到堆叠之中时,ESP暂存器会有 麼变化
5. (是非题)在使用Irvine32函 库时,只有32位元的资 可以被PUSH到堆
叠中.
6. (是非题)在使用Irvine16函 库时,只有16位元的资 可以被PUSH到堆
叠中.
7. (是非题)在程序之中的区域变 会被用堆叠的方式建 .
8. (是非题)PUSH指 能有即时运算元.
9. 那一个指 会将一般用途的32位元暂存器全部存到堆叠中
10. 那一个指 会将32位元的EFLAGS暂存器存到堆叠中
11. 那一个指 会将堆叠中的资 POP到EFLAGS暂存器中
12. 挑战:另一个组译器(称为NASM)允许PUSH指 使用特定的暂存器.为
麼这个方法有可能会比在MASM中的PUSHAD指 还要好呢 以下有一
个 子:
PUSH EAX EBX ECX
5-34 Intel 组合语言
5.5 定义及使用程序
5.5 Defining and Using Procedures
如果你已经研 过任可一种高阶程式语言的话,你应该知道程式是可以被
分成一段段的称为函 的 辑单位.在撰写程式时,任何复杂的问题 应被分
解为一系 较小的单元,我们才能 解整个程式应有的架构,并将它们一个个
实作出 ,这样子在测试时会 有效 .在组合语言中,我们基本上使用 普
遍的名词,程序(procedure), 代表和上述相同的做法.
假如你定位在以物件导向的方法 撰写你的程式,你可以将在一个组合语
言原始码模组中的程序集合及 据资 ,当成是在一个单一 别之中的所有函
.组合语言的出现是远早於物件导向程式设计的, 所当然地,它并 拥有
像在C++,JAVA或任何相似语言中可以看到的结构形式.你可以自 决定是否
在你的程式之中采用任何你认为需要的结构形式.
5.5.1 PROC指引
5.5.1.1定义程序
较 正式的 ,我们定义一个程序(procedure)为一个被命名的叙述
(statements)区块,并以返回叙述(return)做为结尾.程序的宣告使用PROC及
ENDP指引.它必需指定一个名称(一个有效的 别字).到目前为止,我们写
的每一个程式 包含一个名为main的程序, 如:
main PROC
.
.
main ENDP
当你要设计一个程序, 同於你的程式的启始程序时,记得在结尾处使用
RET指 .
第5章 程序 5-35
sample PROC
.
.
ret
sample ENDP
启始程序(即main)是一个特 ,因为它会以exit叙述结束.当你使用
INCLUDE Irvine32.inc叙述时,exit是呼叫ExitPrecess程序的别名,它是一个
MS-Windows用 结束程式的函 (在章节8.3.1中,我们会介绍INVOKE指引,它可
以用 呼叫程序及传递引 ):
INVOKE ExitProcess, 0
假如你使用的INCLUDE Irvine16叙述,那麼exit会被转译为.EXIT组译器
指引.后者会让组译器产生下 二个指 :
mov ah,4Ch ; 呼叫 MS-DOS函 4Ch
int 21h ; 结束程式
5.5.1.2范 :三个整 的和
让我们 定义一个名为SumOf的程序, 记算三个32位元的整 的和.
我们将会假设这三个相关整 在这个程序被叫用之前,会先被指派到EAX,EBX
及ECX之中.这个程序会将计算出 的和回传到EAX中:
SumOf PROC
add eax, ebx
add eax, ecx
ret
SumOf ENDP
5-36 Intel 组合语言
5.5.1.3程序的注解 明
在撰写程式时应该要养成为程式加上清楚且 的注解 明.以下有一些
关於你可以加在每一个程序之前的 明资讯的建议:
一个包含这个程序所完成的每项工作的 明.
表 明所有的 及它们的使用方式,并标上像是接收 之 的标
签.假如任何一个输入的 有特定的规格要求,也在这 加上 明.
对所有程序的回传值一一 明,并标上 似返回值的标签.
将呼叫这个程序所需要的特殊需求,我们称之为先决条件
(Preconditions),这些需求必需要呼叫程序之前被满足.我们可以为它
标上一个 似呼叫需求的标签. 如,对一个用 画出直线的程序而言,
让视讯显示装置处於绘图模式下是一个必须的先决条件.
我们在上面所选用的 明标签,比如 接收 ,回传值及呼叫需求,并
是绝对的;其他有 於辨 的名字也常被使用.
由以上提到的几个重点,让我们 替SumOf程序加上适当的注解 明吧:
;---------------------------------------------------------
SumOf PROC
;
; 计算并回传三个整 的总和.
; 接收 : EAX, EBX, ECX, 三个带正号或负号的整
; 回传值: EAX = 总和, 及其他 态旗标 (如进位,溢位等) 会被改变.
;---------------------------------------------------------
add eax,ebx
add eax,ecx
ret
SumOf ENDP
第5章 程序 5-37
5.5.2 CALL及RET指
CALL指 用 呼叫程序,它会指示处 器在一个新的记忆体位置开始执
.程序会 用RET(Return From Procedure)指 将处 程序带回到程式呼叫
程序的那个点上.就运作机制而言,CALL指 将它的返回位址push到堆叠之
中,并将所要呼叫的程序位置复制到指 指标暂存器中.当程序执 完成准备
回传资 时,程序中的RET指 会由堆叠之中取出先前存入堆叠之中的返回位
址并写入指 指标暂存器.CPU总是在执 在记忆体之中由EIP指向的指 .
EIP称为指 指标暂存器(Instruction Pointer Register, or IP),为16位元模式.
5.5.2.1呼叫及回传的使用范
假设在main之中,CALL的指 位在偏移值00000020的位址上.一般而
言,这个指 的机器码会需要5个位元组的空间,所以下一个叙述(在此 中
是一个MOV的指 )会出现在偏移值00000025的位址上:
main PROC
00000020 call MySub
00000025 mov eax, ebx
接下 ,假设在MySub中第一个被执 的指 是在偏移值00000040的位址
上:
MySub PROC
00000040 mov eax, edx
.
.
ret
MySub ENDP
5-38 Intel 组合语言
当CALL指 被执 时,接在CALL之后的位址(00000025)会被PUSH
到堆叠中,而MySub的位址会被 进EIP之中,如下图所示:
所有在MySub中的指 会在执 完后由RET指 回传其执 的结果.当
RET指 被执 时,在堆叠中由ESP所指到的值,会被POP到EIP之中.正如
下图所示,这样子会让处 器由偏移位址00000025处继续执 ,即在程序呼叫
之后,下一个指 的位置:
第5章 程序 5-39
5.5.2.2巢 程序呼叫
巢 程序呼叫(Nested Procedure Call)是指当我们在一个已经被呼叫的程
序之中,还未执 完成并回传之前,又叫用 另一个程序.假设main呼叫 一
个名为Sub1的程序.当Sub1正在执 时,又呼叫 Sub2程序.当Sub2执
时,又呼叫 Sub3程序.其过程如下图所示:
5-40 Intel 组合语言
当在Sub3结尾处的RET指 执 时,它会将在stack[ESP]中的值POP到
指 指标之中.这会使得程式的执 由call Sub3这个指 之后的位置继续执
下去.下 的图示之中,在Sub3回传动作执 前,堆叠之中的情形:
在回传之后,ESP会指向在堆叠之中下一个最高点的位置.当在Sub2结尾
处的RET指 将要被执 时,堆叠中的情形会如下图所示:
最后,当Sub1回传时,stack[ESP]会被POP到指 指标,并且回到main
之中,继续执 动作:
很明显地,堆叠是很有用处的,它可以用 当作记载包含巢 程序呼叫资
讯的装置.一般而言,堆叠结构会被用在程式必需以特定顺序返回它们的步骤
的情形.
第5章 程序 5-41
5.5.2.3区域标签及全域标签
在预设的情形下,程式标签(Code Label)(尾随著单个冒号的)会拥有区
域的有效范围,这个使得它只能被包含在它所属程序之下的叙述所使用.这可
以避免你JUMP或到LOOP到现 程序之外的标签上.在一些 常发生的情形
之下,你真的必需将控制权转交到一个现 程序之外的标签上时,这个标签就
必需要宣告成全域的.要宣告成全域时,在标签后方加上二个冒号. 如:
GlobalLabel::
在下 的程式片段中(取自Jumps.asm),由main中JUMP到L2时会产
生一个语法错误,因为L2是Sub2程序之中的区域标签.相反地,由Sub2之
中JUMP到L1则是合法的,因为L1被定义为全域标签:
main PROC
jmp L2 ; 会发生语法错误
L1:: ; 全域标签(global label)
exit
main ENDP
sub2 PROC
L2: ; 区域标签(local label)
jmp L1 ; 合法
ret
sub2 ENDP
5-42 Intel 组合语言
5.5.2.4传递暂存器引 到程序中
如果你想要撰写一个一般用途的程序,比如 :计算一个整 阵 中所有
整 的和,那麼在程序之中载入指定特定变 名称 照的方式并 是一个好的
做法.当你用那样的方法撰写时,你的程序 无法用 处 一个以上的阵 .
处 这 问题时较好的方法是,传入阵 的偏移值到程序之中,然后再传一个
整 确定阵 所包含的元素个 .我们将这些称之为引 (Arguments)(或输
入 (Input Parameters)).在组合语言之中,把一般用途的暂存器当做引
传递是很常 的.
在之前的章节之中,我们曾撰写过一个名为SumOf的简单程序,它可以用
加总在EAX,EBX及ECX暂存器中的值.在main中,我们在SumOf之前
先指定EAX,EBX及ECX的值:
data
theSum DWORD
.code
main PROC
mov eax, 10000h ; argument
mov ebx, 20000h ; argument
mov ecx, 30000h ; argument
call SumOf ; EAX = (EAX + EBX + ECX)
mov theSum, eax ; save the sum
在CALL的指 之后,我们可以选择将在EAX暂存器中的加总结果复制到
任何一个我们想要指定的变 之中.
第5章 程序 5-43
5.5.3 范 :计算整 阵 的和
有一种非常常 的回圈 型,你或许已经使用C++ 或是JAVA撰写过,那
就是计算一个整 阵 中所有整 的和.这个程式用组合语言 实作是非常容
的,而且,还有特别的写法可以让程式跑的 快. 如,其中一种就是在回
圈中使用暂存器 取代变 .
让我们 撰写一个程序,定名为ArraySum,它可以由呼叫它的程式之中
接收二个 :一个指向32位元整 的整 阵 ,及一个为整 元素总 的值.
它会记算并回传所有在阵 中整 的和到EAX之中:
;-----------------------------------------------------
ArraySum PROC
;
; 计算在一个32位元整 阵 中所有整 的合
; 接收 :ESI = 阵 的offset
; ECX = 阵 之中整 的个
; 回传值 :EAX = 阵 中所有元素的和
;-----------------------------------------------------
push esi ; 储存ESI, ECX
push ecx
mov eax, 0 ; 将总和初始化为
L1:
add eax, [esi] ; 将每一个整 加到总和之中
add esi, 4 ; 指向下一个整
loop L1 ; 重覆回圈
pop ecx ; 回 ECX, ESI
pop esi
ret ; 将总和传到EAX中
ArraySum ENDP
在这个程序之中,没有任何一个叙述会限定只能存取某一个特定的阵 名
称或是阵 大小.所以ArraySum此程序可以被用在任何一个程式之中,只要那
个程式需要计算一个32位元整 阵 的所有整 和.只要有可能办得到,我们
应该撰写和这个程序一样具有弹性及适应性的程序.
5-44 Intel 组合语言
呼叫ArraySum
接下 的是一个呼叫ArraySum的 子,将array的位址传进ESI中,并且
将其元素总 值传到ECX之中.在呼叫之后,我们将在EAX中的总和复制到一
个变 之中:
.data
array DWORD 10000h, 20000h, 30000h, 40000h, 50000h
theSum DWORD
.code
main PROC
mov esi, OFFSET array ; ESI 指向array
mov ecx, LENGTHOF array ; ECX = 阵 的长 (即其元素总 )
call ArraySum ; 计算其和
mov theSum, eax ; 回传到 EAX
5.5.4 程图
程图(Flowchart)是一个图示程式 辑操作众所皆知的方法.每一个
程图的基本图形符号代表著单一 辑步骤,而有著箭头的, 接这些图形的线
条,代表著 辑步骤的进 顺序.图5-1中是最常 的几个 程图中的图形符号.
图5-1 基本 程图图形符号
是
否
决策
程序呼叫
处 (工作)
开始/结束
第5章 程序 5-45
加在决策符号外的文字标记,像是「是」及「否」,用 代表分支的方向.
每一个加在决策符号的箭号,并没有规定要加在那一个位置或朝那一个方向.
每一个处 符号并可以包含一个或多个比较接近,相关的指 .在这 写的指
并 一定要求要语法正确. 如,我们可以用下 任一种的处 符号代表加
一到CX:
让我们用前一章节的ArraySum程序 设计一个简单的 程图,如图5-2
所示.注意它使用决策符号 代表LOOP,因为LOOP必需检验是否要继续转交
控制权到标签的位址去(依据CX的值而定).插入的程式码代表原始程序内容.
5-46 Intel 组合语言
5.5.5 储存及回 暂存器
你或许已经注意到在ArraySum的范 之中,ECX及ESI在程序开始执
时会被PUSH到堆叠之中,而在其结束时会被POP出 .对大部份的程序 ,
这是一种典型的处 暂存器的方式.千万记得要储存及回 呼叫程序所需用到
的暂存器,这样子可以确保在呼叫程序的那个程式之中所有的暂存器 会被
重写并覆盖掉.
5.5.5.1 USES运算子
USES运算子,和PROC指引结合在一起,可以让你 出所有在程序之中使
用到的暂存器.这个指示组译器做下 二件事,一是产生PUSH指 ,在程序
开始之初将暂存器储存到堆叠之中,二是产生POP指 在程序结束时将暂存器
的值回 .USES运算子必需紧跟在PROC后面,而在USES之后,同一 之中,
出所有暂存器,暂存器间以空白键或TAB键分开(注意, 是逗号!).
让我们 修改在5.5.3节中的ArraySum程序.原本它使用PUSH及POP
指 储存及 原ESI及ECX,因为这二个暂存器被程序所使用.现在,我们
可以让USES运算子做同样的事:
ArraySum PROC USES esi ecx
mov eax, 0 ; 初始化总合(eax)为
L1:
add eax, [esi] ; 将每一个整 加到总和之中
add esi, 4 ; 指向下一个整
loop L1 ; 重复动作直到阵 结束
ret ; 总合的值回传到EAX中
ArraySum ENDP
第5章 程序 5-47
经过上面的修改,下 的编码会被组译器产生:
ArraySum PROC
push esi
push ecx
mov eax, 0 ; 初始化总合为
L1:
add eax, [esi] ; 将每一个整 加到总和之中
add esi, 4 ; 指向下一个整
loop L1 ; 重复动作直到阵 结束
pop ecx
pop esi
ret
ArraySum ENDP
除错诀窍:如果你使用像是Microsoft Visual Studio的除错器,你可以检视由MASM
进阶运算子及指引所产生的隐藏机器指 .由[检视]选单中选择[除错视窗],并点
选[反组译].这个视窗会显示出你的程式原始码及所有由组译器所产生的隐藏机
器指 .
( 外
当程序使用一个暂存器 回传一个值时,有一个和我们储存暂存器的固定
规则有关的重要 外情形会发生.在这个特 当中,用 回传的暂存器 应被
PUSH及POP.举 ,在SumOf程序当中,当我们PUSH并且POP EAX
时,程序的回传值 会消失:
SumOf PROC ; 三个整 的和
push eax ; 储存 EAX
add eax, ebx ; 计算EAX, EBX, ECX
ad eax, ecx ; 的总合
pop eax ; 总合值消失!
ret
SumOf ENDP
5-48 Intel 组合语言
5.5.6 自我评
1. (是非题)PROC指引启始一个程序,而ENDP指引结束一个程序.
2. (是非题)在一个存在的程序当中再定义一个程序是可 的.
3. 在一个程序之中如果RET指 被遗 会发生 麼事
4. 接收 (Receives)及回传值(Returns),这二个字在建议的程序注解
明中代表 麼意思
5. (是非题)CALL指 会将该指 的偏移值PUSH到堆叠之中.
6. (是非题)CALL指 会将接在该指 后的下一个指 的偏移值PUSH到
堆叠之中.
7. (是非题)RET指 会由堆叠之中POP到指 指标暂存器中.
8. (是非题)巢 程序呼叫 被Microsoft的组译器所允许,除非在程序定义
中使用NESTED运算子.
9. (是非题)在保护模式下,每一个程序呼叫 会 用至少4个位元组的堆
叠空间.
10. (是非题)当传 到程序中时,ESI及EDI暂存器无法被使用.
11. (是非题)ArraySum程序(章节5.5.3)可以接收一个指向任何双字阵
的指标.
12. (是非题)USES运算子允许你为所有在程序之中被修改的暂存器命名.
13. (是非题)USES运算子只会产生PUSH指 ,因此你必需要自 加上POP
指 .
14. (是非题)在USES指引 出的暂存器名称必需使用逗号分隔.
15. 在ArraySum程序(章节5.5.3)之中,那一个叙述经过修改可以使得它能
处 16位元字符的阵 展示之.
第5章 程序 5-49
5.6 使用程序设计程式
5.6 Program Design Using Produres
除 一些较单纯的程式(比如 HelloWorld之 的程式)以外,大部份的
应用程式设计 包含 一些 同的步骤.将所有的程式码写在一个程序之中是
可以的,但是我们很快会发现这样的一个程式会很难阅 ,也很难维护.取而
代之的,我们会将 同的程式步骤分割成许多个别独 的程序.这些程序可以
全部放在同一个原始程式码档案中,或者,也可以分别放在多个档案中组成一
个程式.
当你开始撰写一个程式时,最好是能有一套完整的 明或规格,确 地
出这个程式到底是用 做 麼用的.这个 明书,通常是对一个在真实世界中
需要被解决的问题,仔细分析的结果.将这个 明书当做是一个开始点,你就
可以开始设计你的程式 .
一个标准的设计方法是,将一个完整的问题,分成一个个 同的任务,而
每一个任务 可以被编码为一个单独的程序.像这样子,将一个问题拆成一个
个任务的方式,我们通常称为功能性分解(Functional Decomposition)或由上而
下设计(Top-down Design).以下有几点假设是这个方法所包含的:
将一个大的问题分成一个个小的任务,会 容 处 .
在一个程式中,如果每一个程序可以被分别的测试,那它将 容 维护.
由上而下的程式设计方式能让你 解程序之间彼此的关系为何.
当你确认 全盘的设计后,你可以 容 地专心处 细节部份,将每个程
序编码,实作出 .
在下一个章节 ,我们将使用Top-down的方法 设计并实作出一个相当简
单的问题(加总整 )的解决方案.同样的方法可以用 设计 复杂的程式.
5-50 Intel 组合语言
5.6.1 设计整 加法程式
以下是一个简单的程式的 明,我们称之为整 加法:
写一个允许使用者输入一个或 多个32位元整 的程式,并将这些整 存到
一个阵 之中,计算阵 之中所有整 的总和,然后将总和显示在萤幕上.
下 的虚拟码 明 我们可以如何将这个程式的规格划分为几个工作:
Integer Summation Program ; 整 加总程式
Prompt user for three integers ; 提示使用者输入三整
Calculate the sum of the array ; 计算阵 值的总合
Display the sum ; 显示总合结果
在准备开始撰写程式时,我们会先为每一个工作指派一个程序名称:
Main
PromptForIntegers
ArraySum
DisplaySum
在组合语言之中,输入输出的工作常需要详细的编码才能实做出 .为
减少一些细节的部份,我们可以呼叫程序 清除萤幕,显示字 ,输入一个整
及显示一个整 :
Main
Clrscr ; 清除萤幕
PromptForIntegers
WriteString ; 显示字
ReadInt ; 输入整
ArraySum ; 计算整 和
DisplaySum
WriteString ; 显示字
WriteInt ; 显示整
第5章 程序 5-51
结构图 (Structure Chart)
下 的图型称之为结构图,用 描述程式的结构.其中使用到 络函 库
的程序,以阴影表示之:
根程式 (Stub Program)
在将程式实作出 之前,让我们先设计一个缩小版本(最小化)的程式,
称之为根程式(指程式将从此根发芽成长).它只包含空的(或是近乎空的)程序.
这个程式在组译后可以执 ,但实际上 会做出任何有用的事情:
TITLE Integer Summation Program (Sum1.asm)
; 这个程式由使用者输入多个整
; 并将之存到一个阵 之中,计算其总和
; 并在萤幕上显示其结果
INCLUDE Irvine32.inc
.code
main PROC
; 主程式控制程序
; 呼叫程序: Clrscr, PromptForIntegers,
; ArraySum, DisplaySum
exit
main ENDP
;-----------------------------------------------------
PromptForIntegers PROC
;
; 提示使用者一个整 阵 ,并将
5-52 Intel 组合语言
; 使用者的输入值填入阵 之中.
; 接收 : ESI 指向一个双字整 阵
; ECX = 阵 大小.
; 回传值: 回传
; 呼叫程序: ReadInt, WriteString
;-----------------------------------------------------
ret
PromptForIntegers ENDP
;-----------------------------------------------------
ArraySum PROC
;
; 计算一个32位元整 阵 的整 和.
; 接收 : ESI 指向阵 , ECX = 阵 大小
; 回传值: EAX = 阵 元素总和
;-----------------------------------------------------
ret
ArraySum ENDP
;-----------------------------------------------------
DisplaySum PROC
;
; 在萤幕上显示总和.
; 接收 : EAX = 总和
; 回传值: 回传
; 呼叫程序: WriteString, WriteInt
;-----------------------------------------------------
ret
DisplaySum ENDP
END main
存根模组程式可以让你有机会安排所有的程序呼叫,检视程序之间的依存
关系,有可能的话,在编写细节部份的程式码前,改善程式结构的设计.因此,
很清楚地,你应该为每一个程序加上注释, 明它的作用及所需要的 .
第5章 程序 5-53
5.6.1.1 整 加法程式实作
该是完成加法程式的时候 .在资 段宣告一个整 阵 ,使用一个符号
名当做阵 的大小:
IntegerCount = 3
array DWORD IntegerCount DUP( )
有二个字 要被用 当作萤幕上的提示:
prompt1 BYTE "Enter a signed integer: ", 0
prompt2 BYTE "The sum of the integers is: ", 0
在main程序中会清除萤幕,传递一个阵 指标给PromptForIntegers程
序,呼叫ArraySum,并呼叫DisplaySum:
call Clrscr
mov esi, OFFSET array
mov ecx, IntegerCount
call PromptForIntegers
call ArraySum
call DisplaySum
PromptFroIntegers呼叫WriteString 提示使用者输入一个整 .它接著
会呼叫ReadInt接收使用者的输入,并将整 存到ESI所指向的阵 之中.
回圈会让这些步骤执 多次.
ArraySum计算并回传阵 中所有整 的和.
DisplaySum会在萤幕上显示出一段讯息("The Sum of the integers is:")并
呼叫WriteInt 显示在EAX中的整 .
完整程式表
下 程式码是整 加法程式的完整程式码:
5-54 Intel 组合语言
TITLE Integer Summation Program (Sum1.asm)
; 这个程式由使用者输入多个整 , 并将之存到一个阵 之中,计算其总和
; 并在萤幕上显示其结果
INCLUDE Irvine32.inc
INCLUDE Irvine32.inc
IntegerCount = 3 ; 阵 大小
.data
prompt1 BYTE "Enter a signed integer: ",0
prompt2 BYTE "The sum of the integers is: ",0
array DWORD IntegerCount DUP( )
.code
main PROC
call Clrscr
mov esi, OFFSET array
mov ecx, IntegerCount
call PromptForIntegers
call ArraySum
call DisplaySum
exit
main ENDP
;-----------------------------------------------------
PromptForIntegers PROC
; 提示使用者一个整 阵 ,并将使用者的输入值填入阵 之中.
; 接收 : ESI 指向阵 , ECX = 阵 大小.
; 回传值: 回传
; 呼叫程序: ReadInt, WriteString
;-----------------------------------------------------
pushad ; 储存所有暂存器
mov edx, OFFSET prompt1 ; 命 提示的位址
L1:
call WriteString ; 显示字
call ReadInt ; 将整 进EAX中
call Crlf ; 换
mov [esi], eax ; 存到阵 之中
add esi, 4 ; 下一个整
loop L1
popad ; 原所有暂存器
ret
PromptForIntegers ENDP
第5章 程序 5-55
;-----------------------------------------------------
ArraySum PROC
;
; 计算一个32位元整 阵 的整 和.
; 接收 : ESI 指向阵
; ECX = 阵 元素个
; 回传值: EAX = 阵 元素总和
;-----------------------------------------------------
push esi ; 储存ESI及ECX
push ecx
mov eax,0 ; 将总和归
L1:
add eax,[esi] ; 将每一个整 加到总和之中
add esi,4 ; 指向下一个整
loop L1 ; 重 到阵 结束
pop ecx ; 原ESI及ECX的值
pop esi
ret ; 总和结果在EAX中
ArraySum ENDP
;-----------------------------------------------------
DisplaySum PROC
;
; 在萤幕上显示总和.
; 接收 : EAX = 总和
; 回传值: 回传
; 呼叫程序: WriteString, WriteInt
;-----------------------------------------------------
push edx
mov edx, OFFSET prompt2 ; 显示讯息
call WriteString
call WriteInt ; 显示EAX的值
call Crlf
pop edx
ret
DisplaySum ENDP
END main
5-56 Intel 组合语言
5.6.2 自我评
1. 将一个大的工作分成 个较小的部份的过程我们称之为 麼
2. 在加法程式设计之中,那些程序是 向自Irvine32函 库中.
3. 麼是Stub Program
4. (是非题)在加法程式(章节5.6.1.1)中的ArraySum程序直接 照一个
阵 名称变 .
5. 在加法程式(章节5.6.1.1)中的PromptForIntegers程序 ,对那一 做
修改后,可以让程式能够处 16位元的字元 展示之.
6. 画出在加法程式中,PromptForIntegers程序的 程图( 程图在章节5.5.4
中介绍过).
第5章 程序 5-57
5.7 本章摘要
5.7 Chapter Summary
在本章之中介绍 本书的 结函 库,它可以让你在组合语言的应用程式
中,处 输入及输出时 容 些.
在表5-1 出 Irvine32 结函 库中大部份的程序.最新版的程序会在
本书的网站中做定期的 新,你可以到那儿下载.
在章节5.3.3中的函 库测试程式,展示 一些在Irvine32函 库中输入输
出的函 .它会产生并显示出 出的 ,暂存器倾印及记忆体倾印.它以
同的格式显示整 ,并展示 字 的输入及输出.
执 时间堆叠是一个特别的阵 ,用 当作一个暂时的存放空间,储存位
址或资 .ESP暂存器会在堆叠中的某处,拥有一个32位元的偏移值.堆叠称
为LIFO结构(后进先出),因为最后存进堆叠中的值会最先被取出.PUSH运
算会将一笔资 复制到堆叠之中.POP运算会由堆叠之中移除一笔资 ,并将
它复制到一个暂存器或变 之中.堆叠经常用 保存程序的回传位址,程序
,区域变 及在程序内部所使用的暂存器.
PUSH指 会先减少堆叠指标的值,然后将 源运算元复制到堆叠之中.
POP指 会先复制堆叠之中由ESP所指向的内容,并将之复制到一个16或32
位元的目的运算元,然后减少ESP的值.
PUSHAD指 会将一般用途的32位元暂存器PUSH到堆叠之中,而PUSHA
指 也是一样的作用,只是它是用 对16位元的暂存器.POPAD指 会由堆叠
中POP资 到一个一般用途的32位元暂存器,而POPA指 也是一样的作用,
但是它用 POP到16位元的暂存器.
PUSHFD指 会PUSH一个32位元的EFLAGS暂存器到堆叠中,而POPFD
则会由堆叠中POP资 到EFLAGS.PUSHF及POPF的作用相同,但它们只适
用16位元的FLAGS暂存器.
5-58 Intel 组合语言
在章节5.4.2.5中介绍的RevString程式, 用堆叠 反转一个字 中的字
元顺序.
程序是在程式当中被命名的一段区块,宣告时使用PROC及ENDP指引
宣告.程序一定要以RET指 当作它的结尾.在章节5.5.1.2中的SumOf程
序,用 计算三个整 的和.CALL指 会藉由将程序的位址插入到指 指标暂
存器中 执 程序.当一个程序完成后,程序结尾处的RET指 (Return From
Procedure)会将处 器带回到程式之中,程序被呼叫的位置上.巢 程序呼叫,
是指在一个被呼叫的程序之中,在其完成并回传之前,再呼叫另一个程序.
在预设的情形下,程式标签(跟著一个冒号)会被当成是区域标签,其有
效区为其所在的程序之中.而跟著二个冒号的程式标签则是全域标签,其有效
区域为其所在的程式档案中.
章节5.5.3之中的ArraySum程序,会计算并回传一个阵 中所有 字的总
和.
伴随著PROC指引的USES运算子可以让你 出该程序所使用的暂存器.
组译器会在程序的开头及结尾处产生一些程式码,将这些暂存器在程序开始
时,PUSH到堆叠之中,并在结束时POP出 .
任何大小的程式 应该藉由程式 明 小心的设计.标准的方法是使用功
能性分解(由上而上的设计方式) 将程式分解为 个程序.首先,先确认程
序之间的 结关系及顺序,然后再填写程序的细节内容.
第5章 程序 5-59
5.8 程式设计 习
5.8 Programming Execises
1. 改变文字颜色
使用书中的 结函 库 撰写一个程式,让一个字 以四种 同的颜色显
示在萤幕上.
2. 整 阵 输入
撰写一个程式,使用回圈让使用者输入十个32位元的整 ,并将之存於在
一个阵 之中,然后再将它们显示在萤幕上.
3. 简单的加法(1)
撰写一个程式,先清除萤幕,然后将游标定位在萤幕的正中央.提示使用
者输入二个整 ,加总它们并显示其总和於萤幕上.
4. 简单的加法(2)
使用前一个 习的程式当作开端.使用回圈,让这个新的程式可以重复同
样的步骤三次,在每次回圈重复时清除萤幕.
5.
撰写一个可以产生50个介於+20到-20之间的 ,并将之显示在萤幕上.
6. 随机字
写一个程式,产生并显示出二十个随机字 ,每一个字 包含十个大写的
字母{A…Z}.
5-60 Intel 组合语言
7. 随机萤幕位置
撰写一个程式,将一个字元显示在萤幕上一百个随机的位置.挑战:如果
可以的话,让字元以10到300ms之间的时间间隔,随机显示.
8. 色彩阵
写一个程式使用所有可能的文字颜色和底色(16×16=256)的组合, 显示
一个单独的字元.颜色的代号是从0到15,因此你可以使用一个巢 回圈 产
生所有可能的组合方式.
Procedures
5. 程 序
5.1 导
5.2 结外部函 库
5.2.1 背景知
5.2.2 自我评
5.3 本书所用的 结函 库
5.3.1 概观
5.3.2 个别程序的描述
5.3.2.1 Irvine32.inc标头档
5.3.3 函 库测试程式
5.3.4 自我评
5.4 堆叠运算
5.4.1 执 时间堆叠
5.4.1.1 Push的运作方式
5.4.1.2 Pop的运作方式
5.4.1.3 堆叠的应用
5.4.2 PUSH及POP指
5.4.2.1 PUSH指
5.4.2.2 POP指
5.4.2.3 PUSHFD及POPFD指
5.4.2.4 PUSHAD,PUSHA,POPAD及POPA
5.4.2.5 范 :反转一个字
5.4.3 自我评
5-2 Intel 组合语言
5.5 定义及使用程序
5.5.1 PROC指引
5.5.1.1 定义一个程序
5.5.1.2 范 :三个整 的和
5.5.1.3 程序的注解 明
5.5.2 CALL及RET指
5.5.2.1 呼叫及回传的使用范
5.5.2.2 巢 程序呼叫
5.5.2.3 区域标签及全域标签
5.5.2.4 传递暂存器引 到程序中
5.5.3 范 :计算整 阵 的和
5.5.4 程图
5.5.5 储存及重置暂存器
5.5.5.1 USES运算子
5.5.6 自我评
5.6 使用程序的程式设计
5.6.1 设计整 加法的程式
5.6.1.1 整 加法程式实作
5.6.2 自我评
5.7 本章摘要
5.8 程式设计 习
第5章 程序 5-3
5.1 导
5.1 Introduction
有几个你应该好好地 完这一个章节的好 由:
你需要学习如何在组合语言的环境之中作输入及输出的动作
你需要学习关於执 时期堆叠(Runtime Stack),以及它如何让我们能够
呼叫函 (function)(我们称之为程序(Procedure))的运作原 .
你的程式将可能愈 愈大,你将会需要将它们做 辑上的分割,将整个程
式变成由一段一段的程序组合而成.
程图(Flowcharts)是用 图形表示出程式的 辑结构的工具.在本章节
中,你将学到如何绘制 程图.
如果你是学生,而本书是你的课堂用书;那麼你的教授很可能会考你这一
章的内容.
5-4 Intel 组合语言
5.2 结外部函 库
5.2 Linking to an External Library
如果你愿意花时间,你可以学会如何写出所有输入和输出的细节程式码,
甚至 最基本的输出入动作也可以办到.那就好像当你每次要使用你的 子的
时候,甚至於自己 可以重新组装 子的引擎一样!那的确是件很有趣的事,
但是实在太花时间 !在本书的后段,第11章的地方,你将会有机会学到如何
在MS-Windows保护模式下进 输入及输出的动作.那将是一个非常地有趣的
过程,尤其当你发现到有那麼多种的工具可以用时,等於开启 一个全新的,
完全 同以往的视野.
目前为止,对刚开始接触组合语言的你而言,输入输出应该是相当地简单
的.在本章的第一个部份将会教导你如何由本书所提供的函 库Irvine32.lib中呼
叫一个程序.本书所提供的函 库的完整程式码可以在书后所附的CD中找到,
在本书的专属网站中也将定期地提供必要的 新.其网址如下:
http://www.nuvisionmiami.com/books/asm/index.html
如果你要写的是在真实位址模式下的16位元程式,在本书中也有附上和
Irvine32.lib内含相同程序的16位元函 库Irvine16.lib.
第5章 程序 5-5
5.2.1 背景知
结函 库(Link Library)是一个包含多个已经被组译成机器码程序的档
案.这些在函 库 的程式码一开始本 是包含 程序,常 及变 的程式码
的原始档.原始档再被组译成目的档,然后再被收集到一个函 库中.
假如你想让你的程式显示出一段字 到萤幕上,你将需要呼叫名为
Writestring的程序.你的程式 将需要包含一个PROTO的指引(directive, 或译
为假指 , 3.1.8), 命名这个要呼叫的程序.以下的指引可以在名为Irvine32.inc
的档案中找到:
WriteString PROTO
接下 , 用CALL指 执 Writestring程序:
call Writestring
当你的程式被组译时,组译器会空下一段记忆体位置给CALL指 ,因为组译
器知道将 这段位置会被 结器填入资 . 结器会在 结函 库中寻找名为
Writestring的程序,并且会将其机器码指 复制到你程式的执 档之中.并且,它还
会将Writestring的位置插入到CALL指 之中填入原先空下 的位置上.
如果你试著要呼叫一个 在 结函 库中的程序, 结器将会显示错误讯
息,并且将 会产生你的程式的执 档.
结器功能选项 (Linker Command Options)
Link 结器可以将你的程式的目的档和一个或多个目的档及函 库做
结.下 的指 是一个范 ,它将hello.obj与irvine32.lib及kernel32.lib函 库
结:
link32 hello.obj irvine32.lib kernel32.lib
先前在书中用 组译并 结的批次档(make32.bat或make16.bat)也是使用
几乎一样的指 .唯一 同的地方是在一个可置换的 (%1),被用 取代
「hello.obj」.这样子做可以让这个批次档用 结任何程式:
link32 %1.obj irvine32.lib kernel32.lib
5-6 Intel 组合语言
整体结构 (Overall Structure)
你也许会对kernel32.lib这个档案在图中的位置感到 解.这个档案是由
Microsoft Windows平台中的Software Development Kit(一般人简称SDK)所提供
的.它包含 用 和作业系统 结的相关资讯,而这些资讯存放在另一个名为
kernel32.dll的档案中.这个档案是MS-Windows作业系统的基本要件,它的名
称叫动态 结函 库(Dynamic Link Library).它包含 许多用 做字元层面
的输入输出的可执 档.你或许可以把kernel32.lib想成是用 和kernel32.dll
沟通的一座桥梁,正如下图所示:
使用你在这章中所学到的东西,你的程式将可 结到Irvine32.lib函 库.
在之后的第11章中,你将会学到如何将你的程式直接地 结到kernel32.lib函
库.
Irvine32.lib你的程式
kernel32.lib
结到
结到
也可 结到
执
kernel32.dll
第5章 程序 5-7
5.2.2 自我评
1. (是非题): 结函 库(link library)中包含 结合语言的原始程式码.
2. 使用PROTO指引(directive, 指引) ,宣告一个存在於外部 结函 库中名为
MyProc的程序.
3. 写一段用CALL指 的程式,呼叫位於一个外部 结函 库中,名为MyPorc
的程序.
4. 书中所提供的三十二位元的 结函 库叫 麼名字
5. 那一个函 库包含由Ivine32.lib函 库呼叫的函 .
6. kernel32.dll是 麼
7. 在批次档make32.bat之中,用 取代档案名称的 叫 麼名字
5-8 Intel 组合语言
5.3 本书所用的 结函 部
5.3 The Book's Link Library
5.3.1 概 观
表5-1是在Irvine32 结函 库之中所包含的程序速查表(一些其他的程
序将在后面的章节再做介绍).首先,一些名词必需要先在此做个解释:
主控台(console):一个在MS-Windows下执 文字模式的三十二位元的主
控视窗.预设值应该包含80 (columns),25 (rows).
标准输入(standard input):标准的输入装置是键盘.但我们仍然可以在命
模式下将标准输入重新导向为任何一个档案或序 埠, 做为标准输入的
源.
标准输出(standard output):标准输出装置是显示器.但我们仍然可以在命
模式下将标准输出重新导向为写入一个档案,印表机或是序 埠.
表5-1 结函 库中的程序
程 序 明
Clrscr 清除主控台,并将游标重新定位於左上角.
Crlf 写入一个 尾标记(end-of-line)到标准输出.
Delay 暂停程式执 n个10-3秒(millisecond, 毫秒)的间隔.
DumpMem
以十 进位的表示方式,将一个区段的记忆体内容写入到标准
输出.
DumpRegs
以十 进位的方式显示EAX,EBX,ECX,EDX,ESI,EDI,
EBP,ESP,EFLAGS及EIP暂存器;同时也显示进位,符号,
值及溢位旗标.
GetCommandtail
复制程式命 的 (也称为command tail)到一个位元组
阵 .
GetMseconds
以毫秒(milliseconds)为单位,回传一个代表过 午夜十二
点多久时间的 值.
第5章 程序 5-9
Gotoxy 在主控视窗中的游标定位到在指定的 .
Random32
产生一个32-bit的假随机(pseudorandom)整 , 值范围在
由0到FFFFFFFF之间.
Randomize 用於产生 种子.
RandomRange 产生一个在一定 值范围内的假随机整 .
ReadChar 由标准输入 入一个单独的字元.
ReadHex
由标准输入( 如Keyboard) 取一个32-bit的十 位元整
,并在接收到[Enter]键时结束 取动作.
ReadInt
由标准输入介面 取一个32-bit的有号十进位整 ,并在接收
到[Enter ]键时结束 取动作.
ReadString
由标准输入 取一个字 ,并在接收到[Enter]键时结束 取动
作.
SetTextColor
设定所有在主控视窗中的文字输出结果的前景及背景的颜
色.(此功能在Irvinel16.lib中并没有提供)
WaitMsg 显示一个讯息,并等待输入键被按下.
WriteBin 以ASCII二进元格式写入一个无号的32-bit整 到标准输出.
WriteChar 写入一个单独的字元到标准输出.
WriteDec 以十进位格式写入一个无号的32-bit整 到标准输出.
WriteHex 以十 进位格式写入一个无号的32-bit整 到标准输出.
WriteInt 以十进位格式写入一个有号的32-bit整 到标准输出.
WriteString 写入一个以Null结尾的字 到标准输出.
5-10 Intel 组合语言
5.3.2 个别程序描述
Clrscr
这个程序是用 清除萤幕用的.基本上会使用在开启及结束一个程式的时
候.假设你想在程式执 过程的其他时机呼叫这个程序,记得在呼叫它之前先
暂停程式的执 态(可以藉由呼叫WaitMsg程序),这可以让使用者在清掉
萤幕前先看完萤幕上所显示的资讯.叫用范 :
call Clrscr
Crlf
这个程序会将标准输出的游标移至下一 的开头.它藉由写入一个包含二
个位元组的字 ,0Dh及0Ah, 达到这样子的效果.叫用范 :
call Crlf
Delay
这个程序被呼叫时会暂停程式的执 态一段特定的时间间隔.当呼叫
时,将EAX以10-3秒(millisecond)为单位,设定你想要暂停的时间间隔.叫用范
(在Irvine16.lib版本中的这个程序无法在Windows NT,2000或XP中执 ):
mov eax,1000 ; 1 秒
call Delay
DumpMem
这个程序会将一段范围内的记忆体内容,以十 进位的格式写入到标准输
出上.当你叫用这个程序时,将启始位置的值传入ESI,单位 值传到ECX
及单位大小值传入EBX(1 = byte,2 = word,4 = doubleword).以下的叫用范
,会显示名为array的阵 ,它包含 11个双字组:
.data
array DWORD 1, 2, 3, 4, 5, 6, 7, 8, 9, 0Ah, 0Bh
.code
main PROC
mov esi, OFFSET array ; 启始偏移值
mov ecx, LENGTHOF array ; 元素个
mov ebx, TYPE array ; doubleword 格式
call DumpMem
第5章 程序 5-11
下 的输出是由上述的范 中的DumpMem所产生的:
00000001 00000002 00000003 00000004 00000005 00000006
00000007 00000008 0000009 0000000A 0000000B
DumpRegs
这个程序会以十 进位格式显示EAX,EBX,ECX,EDX,ESI,EDI,EBP,
ESP,EIP及EFL(EFLAGS)暂存器的值.它同时也显示出进位,符号, 值
及溢位旗标.以下是它显示的范 :
EAX=00000613 EBX=00000000 ECX=000000FF ECX=00000000
ESI=00000000 EDI=00000100 EBP=0000091E ESP=000000F6
EIP=00401026 EFL=00000286 CF=0 SF=1 ZF=0 0F=0
其中EIP所显示的值,是在DumpRegs被呼叫后的下一个指 的偏移值.当
在为程式除错时,DumpRegs是很有用的,因为它可以让你清楚地了解到程式执
中的某一刻CPU的 态.这个程序并 需要输入任何 ,也没有回传值.
( GetCommandtail
这个程序会将程式的命 复制到一个以Null结尾的字 .当命 是空
的时候,进位旗标会被设定,否则,进位旗标会被清除.这个程序当在需要使
用者在程式进 中输入资 时非常好用.
如,假设一个名为Encrypt的程式能够 取一个输入档,档名为file1.txt,
并且会产生一个输出档,名为file2.txt.使用者可以在使用程式时在命 中输
入这二个档名:
Encrypt file1.txt file2.txt
当程式开始时,Encrypt程式可以呼叫GetCommandtail,用 接收这二个档
案名称.当呼叫GetCommandtail,时EDX必需要包含一个至少129 bytes的阵
的偏移值:
.data
cmdTail BYTE 129 DUP(0) ; empty buffer
.code
mov edx, OFFSET buffer
call GetCommandtail ; 填入buffer
5-12 Intel 组合语言
GetMseconds
这个程序会以毫秒milliseconds为单位回传一个 值,代表自从午夜十二点
过后到现在经过 多少时间.当你需要测 在二个事件发生之间经过 多少时
间时,这个程序会是很好用的.回传值会传到EAX中, 需要输入任何的 .
在以下的范 中,我们将呼叫这个程序一次,并储存它的回传值.接下 ,一
个回圈被执 ,最后,我们再一次呼叫GetMseconds程序,然后把得到的二个
时间值相减.我们 得出 确的回圈执 时间,而且是以毫秒为单位.
.data
startTime DWORD
.code
call GetMseconds
mov startTime,eax
L1:
; (在这 执 回圈)
Loop L1
Call GetMseconds
Sub eax,startTime ; EAX = 以毫秒为单位的回圈执 时
间
Gotoxy
这个程序可藉由输入特定的 (row)与 (column)的值 重新定位游标
在主控视窗中的位置.在Windows的主控视窗中,预设的X轴的值(即 或视
窗寛 的值)为0到79,而Y轴的值(即 或视窗高 的值)为0到24.当你
呼叫Gotoxy,你需要将你想要的Y轴的值传入DH中,而X轴的值传入DL中.
叫用范 :
mov dh, 10 ; 10
mov dl, 20 ; 20
call Gotoxy ; 定位游标位置
Random32
这个程序会随机产生一个32-bit的整 并回传其值到EAX中.当此程
序被重复呼叫时,它会产生一组虚拟 序 (Simulated Random Sequence),
其中包含的整 称为假 整 (Pseudorandom Integer)(注:如果你想多 解
第5章 程序 5-13
产生器,请 阅Donald Knuth所著的The Art of Computer Programmming (Vol.2),
Addison-Wesley,1997).这些整 由一个简单的函 所产生,它需要一个输入值称为
种子(Seed).该函 会 用 种子在一个方程式中产生第一个 值,
然后接下 的随机值会以前一个 值当成 种子 产生.一般而言,我们
使用 (Random)这个名词 代表假 (Pseudorandom).叫用范 如下:
.data
randVal DWORD
.code
call Random32
mov randVal,eax
Randomize
这个程序是用 产生 种子,以供Random32及RandomRange二个程序
之中的 产生函 使用. 种子会相等於叫用的时间, 确 到1/100秒.
这很明显地表示当每次你执 一个程式,你所得到的启始 值 将会 同,
并且每一组 序 也将 相同,各自独 .你只需要在程式开始执 之初呼
叫Randomize程序一次就够 .在下 的范 之中,我们将产生十个 值:
call randomize
mov ecx, 10
L1: call random32
;在此处使用或显示在EAX中的 值
Loop L1
RandomRange
这个程序可以产生一个介於0到(n-1)的范围中的 ,其中,n是一个
,将会被输入到EAX暂存器中,产生出 的 值会被回传到EAX之中.
下 的范 会产生一个单独的 ,介於0到4999,并储存到EAX之中:
.data
randVal DWORD
.code
mov eax, 5000
call RandomRange
mov randVal, eax
5-14 Intel 组合语言
ReadChar
这个程序可以用 由标准输入 取一个单独的字元,并将该字元传到AL暂
存器之中.该字元 会被显示在萤幕上.下 是一段叫用的范 程式码:
.data
char BYTE
.code
call ReadChar
mov char,al
ReadHex
ReadHex可以用 在标准输入中, 取一个32-bit的十 进位整 ,并把该
值存放到EAX之中.它 会检查输入的值是否为有效值,你可以输入大写或小
写的A到F 代表 字.输入的最大 字为八位 ,并且无法输入以空白为启
始的 字.以下为叫用范 :
.data
hexVal DWORD
.code
call ReadHex
mov hexVal, eax
ReadInt
ReadInt可以用 在标准输入中, 取一个32-bit的有号整 ,并把回传值
存到EAX之中.使用者可以在开始的第一个位元中输入正,负号,而在其他的
位元中只能输入 字.ReadInt会设定溢位旗标,并且会在输入值无法以32-bit
有号整 的方式(范围:-2,147,483,648到+2,147,483,647)表示时显示错误讯息.
叫用范 如下:
.data
intVal SDWORD
.code
call ReadInt
mov intVal, eax
第5章 程序 5-15
ReadString
这个程序会由标准输入 取一个字 ,并在使用者按下[Enter]键时结束 取
动作.它会计算 取 多少个位元组,并回传计算结果到EAX暂存器之中.在
叫用ReadString之前,必需先将EDX设为字元要输入的阵 位置的偏移值,作
为储存输入字元的缓冲区,并将ECX定义为可 取字元的最大 值.
以下的程式片段会呼叫ReadString,并传入ECX及EDX.要特别注意的是,
我们将ECX的缓冲区值减去一, 扣掉输入字 的Null结尾.
.data
buffer BYTE 50 DUP(0) ; 定义字 长
byteCount DWORD ; 定义字 长 计算变
.code
mov edx, OFFSET buffer ; 指自缓冲区
mov ecx, (SIZEOF buffer) - 1 ; 指定最大 取字 长
call ReadString ; 输入字
mov byteCount, eax ; 字 的长
ReadString程序会自动地在键入字 的结尾加上一个Null字元.以下在使
用者输入一个"ABCDEFG"的字 后,以十 进位格式以及ASCII Dump的
方式,显示缓冲区中的前八个位元组的:
41 42 43 44 45 56 47 00 ABCDEFG
其变 的总位元组 计算结果(byteConut值)会等於7.
5-16 Intel 组合语言
SetTextColor
顾名思义,这个程序可以让我用 设定文字的颜色和底色,下 的表格是
我们可以用 事先定义的文字颜色和底色的选项:
黑 = 0 红 = 4 灰 = 8 淡红 = 12
= 1 洋红 = 5 淡 = 9 淡洋红 = 13
= 2 棕 = 6 淡 = 10 黄 = 14
青 = 3 淡灰 = 7 淡青 = 11白 = 15
这些颜色常 被定义在Irvine32.inc及Irvine16.inc之中.背景的颜色必需
乘上十 后再加到前景颜色之中(代表其值向左平移四个位元,你将会在第7章 到相
关的内容).下 的常 值,举 而言,表示黄色的字在 色的底色上:
yellow + (bulu * 16)
在呼叫SetTextColor之前,需将所要的颜色常 移到EAX之中:
mov eax, white + (blue * 16) ; 白色字, 底色
call SetTextColor
(假如你想阅 多关於显示色彩的资 ,请查阅本书的15.3.2.SetTextColor无法在Irvine16
结函 库中被使用.)
WaitMsg
这个程序会在萤幕上显示"Press [Enter] to continue …"的讯息,并让程式
停止直到使用者按下输入键时才会继续动作.当你希望程式暂停萤幕显示,让
萤幕上的资 能卷动并显示完全时相当好用.它 需要输入任何 .叫用范
如下:
call WaitMsg
WriteChar
这个程序让你可以写入一个单独的字元到标准输出.在呼叫程序之前,需
先将字元传入AL中(或其ACSII码):
mov al, 'A'
call WriteChar ; 显示:"A"
第5章 程序 5-17
WriteDec
这个程序让你能在标准输出中写入一个32-bit的无号整 ,并会以十进位
的方式显示出 ,而且 能以 做为开头.在叫用它之前,先叫整 传入EAX
之中:
mov eax, 295
call WriteDec ; 显示: "295"
WriteHex
这个程序会写入一个32-bit的无号整 到标准输出中,并以八进位的格式
显示.如果有需要,程序会在开头处补上 .在叫用它之前,先将整 传入EAX
中:
mov eax, 7FFFh
call WriteHex ; 显示: "00007FFF"
WriteInt
这个程序会写入一个32-bit的有号整 到标准输出中,以十进位的格式显
示,并在开头处加上正,负号,但 能以 做为开头.在叫用前,先将整 传
入EAX中:
mov eax, 216543
call WriteInt ; 显示: "+216543"
WriteString
这个程序会写入一个以Null做结尾的字 到标准输出中.当叫用它时,先
将字 的缓冲区设到EDX中.叫用范 :
.data
prompt BYTE "Enter your name:", 0
.code
mov edx, OFFSET prompt
call WriteString
5-18 Intel 组合语言
5.3.2.1 Irvine32.inc标头档
以下 出 部份Irvine32.inc,include档的档案内容,它包含 在函 库中
每个程序的原型,同时包含 颜色常 ,结构及符号的定义.这个档案会时常
会 新,所以为确保你所用的是最新的版本,请至本书的专属网页上 新:
; Include file for Irvine32.lib (Irvine32.inc)
INCLUDE SmallWin.inc
.NOLIST
;----------------------------------------
; Procedure Prototypes
;----------------------------------------
ClrScr PROTO
Crlf PROTO
Delay PROTO
DumpMem PROTO
DumpRegs PROTO
GetCommandtail PROTO
GetMseconds PROTO
Gotoxy PROTO
Randomize PROTO
RandomRange PROTO
Random32 PROTO
ReadInt PROTO
ReadChar PROTO
ReadHex PROTO
ReadString PROTO
SetTextColor PROTO
WaitMsg PROTO
WriteBin PROTO
WriteChar PROTO
WriteDec PROTO
WriteHex PROTO
WriteInt PROTO
WriteString PROTO
第5章 程序 5-19
;-----------------------------------
; Standard 4-bit color definitions
;-----------------------------------
black = 0000b
blue = 0001b
green = 0010b
cyan = 0011b
red = 0100b
magenta = 0101b
brown = 0110b
lightGray = 0111b
gray = 1000b
lightBlue = 1001b
lightGreen = 1010b
lightCyan = 1011b
lightRed = 1100b
lightMagenta = 1101b
yellow = 1110b
white = 1111b
.LIST
在这个档案开头的.NOLIST指引可避免这些内容出现在组译器所产生的清
单档中.而在档案尾端的.LIST指引,则会回 清单档的建 动作.在档案一开
始的INCLUDE指引可使得另一个include档(SmallWin.inc)也被引入到组辑器
中.SmallWin.inc这个档案包含 直接呼叫MS-Windows函 时,所要的函 原
型,常 及资 结构.我们将会在第11章时讨 这些内容.
5-20 Intel 组合语言
5.3.3 函 库测试程式
让我们看一下可以用 测试本书所提供的 结函 库中特定程序的小程
式.在程式中的注解 明 每一个步骤:
TITLE Testing the Link Library (TestLib.asm)
; Testing the Irvine32 Library.
INCLUDE Irvine32.inc
CR = 0Dh ; carriage return
LF = 0Ah ; line feed
.data
str1 BYTE "Generating 20 random integers between "
BYTE "0 and 990: ", CR, LF, 0
str2 BYTE "Enter a 32-bit signed integer: ", 0
str3 BYTE "Enter your name: ", 0
str4 BYTE "The following key was pressed: ", 0
str5 BYTE "Displaying the registers: ", CR, LF, 0
str6 BYTE "Hello, ", 0
buffer BYTE 50 dup(0)
dwordVal DWORD
.code
main PROC
; Set text color to black text on white background:
mov eax, black + (white * 16)
call SetTextColor
call Clrscr ; clear the screen
call Randomize ; reset random number sequence
; Generate 20 random integers between 0 and 990.
; Include a 500 millisecond delay.
mov edx, OFFSET str1 ; display message
call WriteString
mov ecx, 20 ; loop counter
mov dh, 2 ; screen row 2
mov dl, 0 ; screen column 0
第5章 程序 5-21
L1: call Gotoxy
mov eax, 991 ; indicate top of range + 1
call RandomRange ; EAX = random integer
call WriteDec ; display in unsigned decimal
mov eax, 500
call Delay ; pause for 500 milliseconds
inc dh ; next screen row
add dl,2 ; move 2 columns to the right
Loop L1
call Crlf ; new line
call WaitMsg ; "Press [Enter]..."
call Clrscr ; clear screen
; Input a signed decimal integer and redisplay it in
; various formats:
mov edx, OFFSET str2 ; "Enter a 32-bit..."
call WriteString
call ReadInt ; input the integer
mov dwordVal, eax ; save in a variable
call Crlf ; new line
call WriteInt ; display in signed decimal
call Crlf
call WriteHex ; display in hexadecimal
call Crlf
call WriteBin ; display in binary
call Crlf
; Display the CPU registers:
call Crlf
mov edx, OFFSET str5 ; "Displaying the registers:"
call WriteString
call DumpRegs ; display registers and flags
call Crlf
; Display a memory dump:
mov esi, OFFSET dwordVal ; starting OFFSET
mov ecx, LENGTHOF dwordVal ; number of units in dwordVal
mov ebx, TYPE dwordVal ; size of a doubleword
call DumpMem ; display memory
call Crlf ; new line
call WaitMsg ; "Press [Enter]..."
; Ask the user to input their name:
5-22 Intel 组合语言
call Clrscr ; clear screen
mov edx, OFFSET str3 ; "Enter your name:"
call WriteString
mov edx, OFFSET buffer ; point to the buffer
mov ecx, SIZEOF buffer - 1 ; max. number characters
call ReadString ; input the name
mov edx, OFFSET str6 ; "Hello, "
call WriteString
mov edx, OFFSET buffer ; display the name
call WriteString
call Crlf
exit
main ENDP
END main
输出范
以下是程式输出的范 .这些 字是 产生的,因此,你的程式输出的
结果可能会和范 的 同:
第5章 程序 5-23
在你按下输入键之后,程式会显示以下的资 :
当你输入你的名字时,程式会再显示一次(最后一个"Press any key"并
是由程式所产生的):
5-24 Intel 组合语言
5.3.4 自我评
1. 在 结函 库中,那一个程序会在固定范围中产生一个
2. 在 结函 库中,那一个程序会显示"Press [Enter] to continue…"并等待使
用者按下[Enter]键
3. 写一段程式让一个程式能暂停700毫秒.
4. 那一个函 库中的程序可以在标准输出中以十进位格式写入一个无号整
5. 那一个程序能将游标定位在视窗中的特定位置
6. 写出当在使用Irvine32函 库时所需要的INCLUDE的指引.
7. 在Irvine32.inc中是 麼型式的程式码
8. DumpMem程序需要输入 麼
9. ReadString程序需要输入 麼
10. 那些程序 态旗标会在DumpRegs程序被叫用时显示出
11. 挑战:写一段程式码,提示使用者一个特定的 字,并输入一个 字字
到一个位元组阵 之中.
第5章 程序 5-25
1
2
3
4
5
6
7
8
9
10
5.4 堆叠运算
5.4 Stack Operations
当你将十个盘子一个一个往上叠,就如同下图所示的方式时,那 是我们
所谓的堆叠(Stacks).假设每一个盘子 相当地重,那麼可想而知的,当我们
想从这个堆叠中间移出一个盘子,是 可能的事,但是我们可以由堆叠的最上
方移出盘子.同 ,要加入一个盘子到堆叠之中也是一样的,我们只能从堆叠
的上方加入,而绝对 可能从中间加入:
顶层
底层
我们把堆叠称做一个LIFO的结构(Last-in,first-out;后进先出).因为
最后一个被加入堆叠的资 ,在从堆叠取出资 时,会被最先取出(LIFO是众所
皆知的名词,但我还是喜欢用盘子 比喻它,因为盘子会提醒我该吃 ).
所有堆叠资 结构(Stack Data Structure) 遵 相同的原则:加入新资
时由堆叠的最上方加入,而 取资 时也是由最上方移出.一般而这,堆叠是
各种程式写作的应用中很好用的一种结构,它可以很容 地以物件导向的程式
方法被实作出 .假如你曾经上过需要 用资 结构的程式写作课程,你应该
已经知道 麼是堆叠抽象资 型态(Stack Abstract Data Type).
然而,在本章之中,我们只会将注意 放在执 时期堆叠(Runtime Stack)
之上.这是直接由CPU中硬体支援的,而且它是叫用和回传程序的原 中,一
个 可缺部份.大部份的时候我们简称它为堆叠(Stack).
5-26 Intel 组合语言
5.4.1 执 时期堆叠
执 时期堆叠是一段直接由CPU管 的记忆体阵 ,它使用 二个暂存
器:SS和ESP.在保护模式下,SS暂存器会保有一个无法以使用者程式改变的
区段描述器(segment descriptor).ESP暂存器则保有一个在堆叠之中某处的32-bit
徧移值.我们很少会直接改变ESP的值,反而是间接地使用一些指 去修改它,
如:CALL,RET,PUSH及POP.
堆叠指标暂存器(Stack Pointer Register)ESP,会指向最后一个加入堆叠
的资 位置,或是最后一个移出堆叠的资 位置.为 明,让我们以一个只
包含一笔资 的堆叠做为开始.在下 的图示中,延伸堆叠指标(Extended Stack
Pointer, ESP)的内容为最新被加入的一笔资 (00000006)的徧移值,一个十
进位的 00001000.
偏移值
ESP00001000 00000006
00000FFC
00000FF8
00000FF4
00000FF0
在图示中,每一个堆叠中的位置 占32个位元,这是因为这个程式是在保
护模式下执 的原故.在实体位址模式下,每一个位置只占16位元,并且由SP
暂存器指向最新加入的资 位置.
第5章 程序 5-27
5.4.1.1 PUSH运算
32-bit的堆叠PUSH运算会将堆叠指标的值减四,并将资 拷贝到堆叠指
标在堆叠之中所指到的位置.如下图所示,我们将PUSH 000000A5这个值到堆
叠之中:
之前 之后
00000006 00001000 ESP0000100000000006
00000FFC ESP 000000A5 00000FFC
00000FF8 00000FF8
00000FF4 00000FF4
00000FF0 00000FF0
或许你已经注意到 ,上图所画的堆叠方向,和我们在本章一开始时所讲的
堆叠方向刚好相反.并没有任何原因造成执 时间堆叠无法在记忆体之中往上做
堆叠,只是,Intel的工程师在设计这部份的内容时,决定让它往下堆叠. 它
是往上还是往下,只要是堆叠 仍旧遵循后进先出的原则!
在PUSH运算前,ESP = 00001000h,之后,ESP = 00000FFCh.下图表示在
相同的堆叠中,再加入二笔新的资 的结果:
Offset
0000100000000006
000000A500000FFC
0000000100000FF8
00000002ESP 00000FF4
00000FF0
5-28 Intel 组合语言
5.4.1.2 POP运算
POP运算会由堆叠之中移除一笔资 ,并将它放进一个 或暂存器之
中.在资 被从堆叠中移出后,堆叠指标会增加并指向下一笔资 的位置.下
的图示 明 00000002这笔资 被由堆叠中移出的之前及之后,堆叠的 态:
之前 之后
00000006 00001000
00000FFC
00000FF8
00000FF4
00000FF0
ESP
00000006 00001000
000000A5 000000A5 00000FFC
00000FF800000001 ESP 00000001
00000002 00000FF4
00000FF0
在ESP以下的堆叠区段我们称作 辑上空的(Logically Empty),这些资
区在同一个程式下一次执 任何一个会填入堆叠动作的指 时被填入新的资
.
5.4.1.3堆叠的应用
以下有几个堆叠在程式中重要的应用方法:
当你希望暂存器能有超过一个以上的用处时,堆叠能提供一个方 的,暂
时的存放区域.当这些暂存器被修改后,它们仍然能回 成原 的值.
当CALL指 被执 时,CPU会将现 程序的返回位址(return address)存
入堆叠之中.
当呼叫一个程序时,我们常会传递要输入的变 ,称为引 (Arguments).
这些引 将被PUSH到堆叠之中.
在程序中的区域变 会被以堆叠之式产生,并在程序结束后被清除.
第5章 程序 5-29
5.4.2 PUSH及POP指
5.4.2.1 PUSH指
PUSH指 会先减少ESP暂存器的值,然后复制一个16位元或32位元的
源运算元到堆叠之中.如果复制的是16位元的运算元,会让ESP值减去2;
而32位元的运算元,会让ESP值减去4.以下是三种指 格式:
PUSH r/m16
PUSH r/m32
PUSH imm32
假如你的程式是要从Irvine32函 库呼叫一个程序,那麼你应该只PUSH
32-bit的资 :否则,函 库所使用的Win32主控台函 会无法正确地执 .如
果你的程式呼叫的是在Irvine16函 库中的程序(在实体位址模式下),你可以
使用16-bit或是32-bit的资 可以.
保护模式下的即时资 (Immediate Values)永远 是32位元的.在真实
模式之下,即时资 的预设值是16位元的,除非你使用 .386处 器指 集
(.386指 集在章节3.2.3介绍过 .)
5.4.2.2 POP指
POP指 会先复制堆叠之中由ESP暂存器所指向的内容,并将它复制到一
个16位元或32位元的目的运算元之中,然后增加ESP的值.当目的运算元是
16位元时,ESP的值会增加2;32位元时,ESP会增加4:
POP r/m16
POP r/m32
5-30 Intel 组合语言
5.4.2.3 PUSHFD及POPFD指
PUSHFD指 会将32位元的EFLAGS暂存器PUSH到堆叠之中,而POPFD
则是由堆叠之中将资 POP到EFLAGS暂存器中:
pushfd
popfd
真实位址模式(Real-Address Mode)程式以PUSHF指 将16位元的FLAGS
暂存器传到堆叠之中,并以POPF指 由堆叠之中取出资 传到FLAGS之中.
有时候,替旗标做个备份,让你在之后可以恢 它们先前的值,在写程式
时会很好用.其中一个方法就是直接将介於PUSHFD及POPFD之间的每一个区
段的程式码围住:
pushfd ; 储存旗标
;
; 在此处的任何程式陈述片段….
;
popfd ; 原旗标
当在使用这种 型的PUSH及POP时,你一定要小心注意到程式的执 并
会跳过POPFD.当一个程式一再被修改时(或是被 新时),你可能会很难
记得每一个PUSH及POP的位置.一个能够达到相同作用,又比较 出错的
方式是将旗标存到变 之中:
.data
saveFlags DWORD
.code
pushfd ; 将旗标PUSH到堆叠中
pop saveFlags ; 复制到变 之中
下 的程式码会将旗标由变 中,回 成原 的值:
push saveFlags ; PUSH已储存的旗标值
popfd ; 复制到旗标之中
第5章 程序 5-31
5.4.2.4 PUSHAD,PUSHA,POPAD及POPA
PUSHAD指 可以将任何一般用途的32位元暂存器,以下 的顺序,PUSH
到堆叠当中:EAX,ECX,EDX,EBX,ESP(原始值),EBP,ESI及EDI.
POPAD指 会将相同的暂存器由堆叠之中,以相反的顺序POP出 .相同地,
PUSHA指 曾在之前介绍80286处 器时介绍过,会将任何一般用途的16位元
暂存器PUSH进堆叠之中,依照下 的顺序:AX,CX,DX,BX,SP(原始值),
BP,SI,DI.POPA指 则会以相反的顺序,由堆叠中POP出以上这些暂存器.
如果你要写一个程序,用 修改一些32位元暂存器,那麼你应该在这个程
序的一开始就使用PUSHAD,并在结尾处使用POPAD 储存并回 这些暂存
器.以下的程式片段是一个 明范 :
MySub PROC
pushad ; 储存一般用途的暂存器
.
.
mov eax, ...
mov edx, ...
mov ecx, ...
.
.
popad ; 回 一般用途的暂存器
ret
MySub ENDP
5-32 Intel 组合语言
5.4.2.5范 :反转一个字
RevString.asm这个程式以回圈方式接收字 ,并将一个个字元PUSH到堆
叠之中.然后再由堆叠中POP(以相反的顺序)出 ,并将之存回原 的字
变 中.因为堆叠是一个LIFO结构(后进先出),所以字 会以相反的顺序显
示:
TITLE Program Template (RevString.asm)
INCLUDE Irvine32.inc
.data
aName BYTE "Abraham Lincoln", 0
nameSize = ($ - aName) - 1
.code
main PROC
; 将姓名PUSH到堆叠中.
mov ecx, nameSize
mov esi, 0
L1: movzx eax, aName[esi] ; 取字元
push eax ; PUSH到堆叠
inc esi
Loop L1
; 以相反顺序由堆叠中POP出姓名,
; 将POP出 的资 存到aName阵 中.
mov ecx, nameSize
mov esi,0
L2: pop eax ; 取字元
mov aName[esi], al ; 存到字
inc esi
Loop L2
; 显示姓名资 .
mov edx, OFFSET aName
call Writestring
call Crlf
exit
main ENDP
END main
第5章 程序 5-33
5.4.3 自我评
1. 那二种暂存器(在保护模式下)用 维护堆叠的存放
2. 执 时间堆叠和堆叠抽象资 型别有那些 同
3. 为何堆叠会被称作为LIFO结构
4. 当一个32位元的值被PUSH到堆叠之中时,ESP暂存器会有 麼变化
5. (是非题)在使用Irvine32函 库时,只有32位元的资 可以被PUSH到堆
叠中.
6. (是非题)在使用Irvine16函 库时,只有16位元的资 可以被PUSH到堆
叠中.
7. (是非题)在程序之中的区域变 会被用堆叠的方式建 .
8. (是非题)PUSH指 能有即时运算元.
9. 那一个指 会将一般用途的32位元暂存器全部存到堆叠中
10. 那一个指 会将32位元的EFLAGS暂存器存到堆叠中
11. 那一个指 会将堆叠中的资 POP到EFLAGS暂存器中
12. 挑战:另一个组译器(称为NASM)允许PUSH指 使用特定的暂存器.为
麼这个方法有可能会比在MASM中的PUSHAD指 还要好呢 以下有一
个 子:
PUSH EAX EBX ECX
5-34 Intel 组合语言
5.5 定义及使用程序
5.5 Defining and Using Procedures
如果你已经研 过任可一种高阶程式语言的话,你应该知道程式是可以被
分成一段段的称为函 的 辑单位.在撰写程式时,任何复杂的问题 应被分
解为一系 较小的单元,我们才能 解整个程式应有的架构,并将它们一个个
实作出 ,这样子在测试时会 有效 .在组合语言中,我们基本上使用 普
遍的名词,程序(procedure), 代表和上述相同的做法.
假如你定位在以物件导向的方法 撰写你的程式,你可以将在一个组合语
言原始码模组中的程序集合及 据资 ,当成是在一个单一 别之中的所有函
.组合语言的出现是远早於物件导向程式设计的, 所当然地,它并 拥有
像在C++,JAVA或任何相似语言中可以看到的结构形式.你可以自 决定是否
在你的程式之中采用任何你认为需要的结构形式.
5.5.1 PROC指引
5.5.1.1定义程序
较 正式的 ,我们定义一个程序(procedure)为一个被命名的叙述
(statements)区块,并以返回叙述(return)做为结尾.程序的宣告使用PROC及
ENDP指引.它必需指定一个名称(一个有效的 别字).到目前为止,我们写
的每一个程式 包含一个名为main的程序, 如:
main PROC
.
.
main ENDP
当你要设计一个程序, 同於你的程式的启始程序时,记得在结尾处使用
RET指 .
第5章 程序 5-35
sample PROC
.
.
ret
sample ENDP
启始程序(即main)是一个特 ,因为它会以exit叙述结束.当你使用
INCLUDE Irvine32.inc叙述时,exit是呼叫ExitPrecess程序的别名,它是一个
MS-Windows用 结束程式的函 (在章节8.3.1中,我们会介绍INVOKE指引,它可
以用 呼叫程序及传递引 ):
INVOKE ExitProcess, 0
假如你使用的INCLUDE Irvine16叙述,那麼exit会被转译为.EXIT组译器
指引.后者会让组译器产生下 二个指 :
mov ah,4Ch ; 呼叫 MS-DOS函 4Ch
int 21h ; 结束程式
5.5.1.2范 :三个整 的和
让我们 定义一个名为SumOf的程序, 记算三个32位元的整 的和.
我们将会假设这三个相关整 在这个程序被叫用之前,会先被指派到EAX,EBX
及ECX之中.这个程序会将计算出 的和回传到EAX中:
SumOf PROC
add eax, ebx
add eax, ecx
ret
SumOf ENDP
5-36 Intel 组合语言
5.5.1.3程序的注解 明
在撰写程式时应该要养成为程式加上清楚且 的注解 明.以下有一些
关於你可以加在每一个程序之前的 明资讯的建议:
一个包含这个程序所完成的每项工作的 明.
表 明所有的 及它们的使用方式,并标上像是接收 之 的标
签.假如任何一个输入的 有特定的规格要求,也在这 加上 明.
对所有程序的回传值一一 明,并标上 似返回值的标签.
将呼叫这个程序所需要的特殊需求,我们称之为先决条件
(Preconditions),这些需求必需要呼叫程序之前被满足.我们可以为它
标上一个 似呼叫需求的标签. 如,对一个用 画出直线的程序而言,
让视讯显示装置处於绘图模式下是一个必须的先决条件.
我们在上面所选用的 明标签,比如 接收 ,回传值及呼叫需求,并
是绝对的;其他有 於辨 的名字也常被使用.
由以上提到的几个重点,让我们 替SumOf程序加上适当的注解 明吧:
;---------------------------------------------------------
SumOf PROC
;
; 计算并回传三个整 的总和.
; 接收 : EAX, EBX, ECX, 三个带正号或负号的整
; 回传值: EAX = 总和, 及其他 态旗标 (如进位,溢位等) 会被改变.
;---------------------------------------------------------
add eax,ebx
add eax,ecx
ret
SumOf ENDP
第5章 程序 5-37
5.5.2 CALL及RET指
CALL指 用 呼叫程序,它会指示处 器在一个新的记忆体位置开始执
.程序会 用RET(Return From Procedure)指 将处 程序带回到程式呼叫
程序的那个点上.就运作机制而言,CALL指 将它的返回位址push到堆叠之
中,并将所要呼叫的程序位置复制到指 指标暂存器中.当程序执 完成准备
回传资 时,程序中的RET指 会由堆叠之中取出先前存入堆叠之中的返回位
址并写入指 指标暂存器.CPU总是在执 在记忆体之中由EIP指向的指 .
EIP称为指 指标暂存器(Instruction Pointer Register, or IP),为16位元模式.
5.5.2.1呼叫及回传的使用范
假设在main之中,CALL的指 位在偏移值00000020的位址上.一般而
言,这个指 的机器码会需要5个位元组的空间,所以下一个叙述(在此 中
是一个MOV的指 )会出现在偏移值00000025的位址上:
main PROC
00000020 call MySub
00000025 mov eax, ebx
接下 ,假设在MySub中第一个被执 的指 是在偏移值00000040的位址
上:
MySub PROC
00000040 mov eax, edx
.
.
ret
MySub ENDP
5-38 Intel 组合语言
当CALL指 被执 时,接在CALL之后的位址(00000025)会被PUSH
到堆叠中,而MySub的位址会被 进EIP之中,如下图所示:
所有在MySub中的指 会在执 完后由RET指 回传其执 的结果.当
RET指 被执 时,在堆叠中由ESP所指到的值,会被POP到EIP之中.正如
下图所示,这样子会让处 器由偏移位址00000025处继续执 ,即在程序呼叫
之后,下一个指 的位置:
第5章 程序 5-39
5.5.2.2巢 程序呼叫
巢 程序呼叫(Nested Procedure Call)是指当我们在一个已经被呼叫的程
序之中,还未执 完成并回传之前,又叫用 另一个程序.假设main呼叫 一
个名为Sub1的程序.当Sub1正在执 时,又呼叫 Sub2程序.当Sub2执
时,又呼叫 Sub3程序.其过程如下图所示:
5-40 Intel 组合语言
当在Sub3结尾处的RET指 执 时,它会将在stack[ESP]中的值POP到
指 指标之中.这会使得程式的执 由call Sub3这个指 之后的位置继续执
下去.下 的图示之中,在Sub3回传动作执 前,堆叠之中的情形:
在回传之后,ESP会指向在堆叠之中下一个最高点的位置.当在Sub2结尾
处的RET指 将要被执 时,堆叠中的情形会如下图所示:
最后,当Sub1回传时,stack[ESP]会被POP到指 指标,并且回到main
之中,继续执 动作:
很明显地,堆叠是很有用处的,它可以用 当作记载包含巢 程序呼叫资
讯的装置.一般而言,堆叠结构会被用在程式必需以特定顺序返回它们的步骤
的情形.
第5章 程序 5-41
5.5.2.3区域标签及全域标签
在预设的情形下,程式标签(Code Label)(尾随著单个冒号的)会拥有区
域的有效范围,这个使得它只能被包含在它所属程序之下的叙述所使用.这可
以避免你JUMP或到LOOP到现 程序之外的标签上.在一些 常发生的情形
之下,你真的必需将控制权转交到一个现 程序之外的标签上时,这个标签就
必需要宣告成全域的.要宣告成全域时,在标签后方加上二个冒号. 如:
GlobalLabel::
在下 的程式片段中(取自Jumps.asm),由main中JUMP到L2时会产
生一个语法错误,因为L2是Sub2程序之中的区域标签.相反地,由Sub2之
中JUMP到L1则是合法的,因为L1被定义为全域标签:
main PROC
jmp L2 ; 会发生语法错误
L1:: ; 全域标签(global label)
exit
main ENDP
sub2 PROC
L2: ; 区域标签(local label)
jmp L1 ; 合法
ret
sub2 ENDP
5-42 Intel 组合语言
5.5.2.4传递暂存器引 到程序中
如果你想要撰写一个一般用途的程序,比如 :计算一个整 阵 中所有
整 的和,那麼在程序之中载入指定特定变 名称 照的方式并 是一个好的
做法.当你用那样的方法撰写时,你的程序 无法用 处 一个以上的阵 .
处 这 问题时较好的方法是,传入阵 的偏移值到程序之中,然后再传一个
整 确定阵 所包含的元素个 .我们将这些称之为引 (Arguments)(或输
入 (Input Parameters)).在组合语言之中,把一般用途的暂存器当做引
传递是很常 的.
在之前的章节之中,我们曾撰写过一个名为SumOf的简单程序,它可以用
加总在EAX,EBX及ECX暂存器中的值.在main中,我们在SumOf之前
先指定EAX,EBX及ECX的值:
data
theSum DWORD
.code
main PROC
mov eax, 10000h ; argument
mov ebx, 20000h ; argument
mov ecx, 30000h ; argument
call SumOf ; EAX = (EAX + EBX + ECX)
mov theSum, eax ; save the sum
在CALL的指 之后,我们可以选择将在EAX暂存器中的加总结果复制到
任何一个我们想要指定的变 之中.
第5章 程序 5-43
5.5.3 范 :计算整 阵 的和
有一种非常常 的回圈 型,你或许已经使用C++ 或是JAVA撰写过,那
就是计算一个整 阵 中所有整 的和.这个程式用组合语言 实作是非常容
的,而且,还有特别的写法可以让程式跑的 快. 如,其中一种就是在回
圈中使用暂存器 取代变 .
让我们 撰写一个程序,定名为ArraySum,它可以由呼叫它的程式之中
接收二个 :一个指向32位元整 的整 阵 ,及一个为整 元素总 的值.
它会记算并回传所有在阵 中整 的和到EAX之中:
;-----------------------------------------------------
ArraySum PROC
;
; 计算在一个32位元整 阵 中所有整 的合
; 接收 :ESI = 阵 的offset
; ECX = 阵 之中整 的个
; 回传值 :EAX = 阵 中所有元素的和
;-----------------------------------------------------
push esi ; 储存ESI, ECX
push ecx
mov eax, 0 ; 将总和初始化为
L1:
add eax, [esi] ; 将每一个整 加到总和之中
add esi, 4 ; 指向下一个整
loop L1 ; 重覆回圈
pop ecx ; 回 ECX, ESI
pop esi
ret ; 将总和传到EAX中
ArraySum ENDP
在这个程序之中,没有任何一个叙述会限定只能存取某一个特定的阵 名
称或是阵 大小.所以ArraySum此程序可以被用在任何一个程式之中,只要那
个程式需要计算一个32位元整 阵 的所有整 和.只要有可能办得到,我们
应该撰写和这个程序一样具有弹性及适应性的程序.
5-44 Intel 组合语言
呼叫ArraySum
接下 的是一个呼叫ArraySum的 子,将array的位址传进ESI中,并且
将其元素总 值传到ECX之中.在呼叫之后,我们将在EAX中的总和复制到一
个变 之中:
.data
array DWORD 10000h, 20000h, 30000h, 40000h, 50000h
theSum DWORD
.code
main PROC
mov esi, OFFSET array ; ESI 指向array
mov ecx, LENGTHOF array ; ECX = 阵 的长 (即其元素总 )
call ArraySum ; 计算其和
mov theSum, eax ; 回传到 EAX
5.5.4 程图
程图(Flowchart)是一个图示程式 辑操作众所皆知的方法.每一个
程图的基本图形符号代表著单一 辑步骤,而有著箭头的, 接这些图形的线
条,代表著 辑步骤的进 顺序.图5-1中是最常 的几个 程图中的图形符号.
图5-1 基本 程图图形符号
是
否
决策
程序呼叫
处 (工作)
开始/结束
第5章 程序 5-45
加在决策符号外的文字标记,像是「是」及「否」,用 代表分支的方向.
每一个加在决策符号的箭号,并没有规定要加在那一个位置或朝那一个方向.
每一个处 符号并可以包含一个或多个比较接近,相关的指 .在这 写的指
并 一定要求要语法正确. 如,我们可以用下 任一种的处 符号代表加
一到CX:
让我们用前一章节的ArraySum程序 设计一个简单的 程图,如图5-2
所示.注意它使用决策符号 代表LOOP,因为LOOP必需检验是否要继续转交
控制权到标签的位址去(依据CX的值而定).插入的程式码代表原始程序内容.
5-46 Intel 组合语言
5.5.5 储存及回 暂存器
你或许已经注意到在ArraySum的范 之中,ECX及ESI在程序开始执
时会被PUSH到堆叠之中,而在其结束时会被POP出 .对大部份的程序 ,
这是一种典型的处 暂存器的方式.千万记得要储存及回 呼叫程序所需用到
的暂存器,这样子可以确保在呼叫程序的那个程式之中所有的暂存器 会被
重写并覆盖掉.
5.5.5.1 USES运算子
USES运算子,和PROC指引结合在一起,可以让你 出所有在程序之中使
用到的暂存器.这个指示组译器做下 二件事,一是产生PUSH指 ,在程序
开始之初将暂存器储存到堆叠之中,二是产生POP指 在程序结束时将暂存器
的值回 .USES运算子必需紧跟在PROC后面,而在USES之后,同一 之中,
出所有暂存器,暂存器间以空白键或TAB键分开(注意, 是逗号!).
让我们 修改在5.5.3节中的ArraySum程序.原本它使用PUSH及POP
指 储存及 原ESI及ECX,因为这二个暂存器被程序所使用.现在,我们
可以让USES运算子做同样的事:
ArraySum PROC USES esi ecx
mov eax, 0 ; 初始化总合(eax)为
L1:
add eax, [esi] ; 将每一个整 加到总和之中
add esi, 4 ; 指向下一个整
loop L1 ; 重复动作直到阵 结束
ret ; 总合的值回传到EAX中
ArraySum ENDP
第5章 程序 5-47
经过上面的修改,下 的编码会被组译器产生:
ArraySum PROC
push esi
push ecx
mov eax, 0 ; 初始化总合为
L1:
add eax, [esi] ; 将每一个整 加到总和之中
add esi, 4 ; 指向下一个整
loop L1 ; 重复动作直到阵 结束
pop ecx
pop esi
ret
ArraySum ENDP
除错诀窍:如果你使用像是Microsoft Visual Studio的除错器,你可以检视由MASM
进阶运算子及指引所产生的隐藏机器指 .由[检视]选单中选择[除错视窗],并点
选[反组译].这个视窗会显示出你的程式原始码及所有由组译器所产生的隐藏机
器指 .
( 外
当程序使用一个暂存器 回传一个值时,有一个和我们储存暂存器的固定
规则有关的重要 外情形会发生.在这个特 当中,用 回传的暂存器 应被
PUSH及POP.举 ,在SumOf程序当中,当我们PUSH并且POP EAX
时,程序的回传值 会消失:
SumOf PROC ; 三个整 的和
push eax ; 储存 EAX
add eax, ebx ; 计算EAX, EBX, ECX
ad eax, ecx ; 的总合
pop eax ; 总合值消失!
ret
SumOf ENDP
5-48 Intel 组合语言
5.5.6 自我评
1. (是非题)PROC指引启始一个程序,而ENDP指引结束一个程序.
2. (是非题)在一个存在的程序当中再定义一个程序是可 的.
3. 在一个程序之中如果RET指 被遗 会发生 麼事
4. 接收 (Receives)及回传值(Returns),这二个字在建议的程序注解
明中代表 麼意思
5. (是非题)CALL指 会将该指 的偏移值PUSH到堆叠之中.
6. (是非题)CALL指 会将接在该指 后的下一个指 的偏移值PUSH到
堆叠之中.
7. (是非题)RET指 会由堆叠之中POP到指 指标暂存器中.
8. (是非题)巢 程序呼叫 被Microsoft的组译器所允许,除非在程序定义
中使用NESTED运算子.
9. (是非题)在保护模式下,每一个程序呼叫 会 用至少4个位元组的堆
叠空间.
10. (是非题)当传 到程序中时,ESI及EDI暂存器无法被使用.
11. (是非题)ArraySum程序(章节5.5.3)可以接收一个指向任何双字阵
的指标.
12. (是非题)USES运算子允许你为所有在程序之中被修改的暂存器命名.
13. (是非题)USES运算子只会产生PUSH指 ,因此你必需要自 加上POP
指 .
14. (是非题)在USES指引 出的暂存器名称必需使用逗号分隔.
15. 在ArraySum程序(章节5.5.3)之中,那一个叙述经过修改可以使得它能
处 16位元字符的阵 展示之.
第5章 程序 5-49
5.6 使用程序设计程式
5.6 Program Design Using Produres
除 一些较单纯的程式(比如 HelloWorld之 的程式)以外,大部份的
应用程式设计 包含 一些 同的步骤.将所有的程式码写在一个程序之中是
可以的,但是我们很快会发现这样的一个程式会很难阅 ,也很难维护.取而
代之的,我们会将 同的程式步骤分割成许多个别独 的程序.这些程序可以
全部放在同一个原始程式码档案中,或者,也可以分别放在多个档案中组成一
个程式.
当你开始撰写一个程式时,最好是能有一套完整的 明或规格,确 地
出这个程式到底是用 做 麼用的.这个 明书,通常是对一个在真实世界中
需要被解决的问题,仔细分析的结果.将这个 明书当做是一个开始点,你就
可以开始设计你的程式 .
一个标准的设计方法是,将一个完整的问题,分成一个个 同的任务,而
每一个任务 可以被编码为一个单独的程序.像这样子,将一个问题拆成一个
个任务的方式,我们通常称为功能性分解(Functional Decomposition)或由上而
下设计(Top-down Design).以下有几点假设是这个方法所包含的:
将一个大的问题分成一个个小的任务,会 容 处 .
在一个程式中,如果每一个程序可以被分别的测试,那它将 容 维护.
由上而下的程式设计方式能让你 解程序之间彼此的关系为何.
当你确认 全盘的设计后,你可以 容 地专心处 细节部份,将每个程
序编码,实作出 .
在下一个章节 ,我们将使用Top-down的方法 设计并实作出一个相当简
单的问题(加总整 )的解决方案.同样的方法可以用 设计 复杂的程式.
5-50 Intel 组合语言
5.6.1 设计整 加法程式
以下是一个简单的程式的 明,我们称之为整 加法:
写一个允许使用者输入一个或 多个32位元整 的程式,并将这些整 存到
一个阵 之中,计算阵 之中所有整 的总和,然后将总和显示在萤幕上.
下 的虚拟码 明 我们可以如何将这个程式的规格划分为几个工作:
Integer Summation Program ; 整 加总程式
Prompt user for three integers ; 提示使用者输入三整
Calculate the sum of the array ; 计算阵 值的总合
Display the sum ; 显示总合结果
在准备开始撰写程式时,我们会先为每一个工作指派一个程序名称:
Main
PromptForIntegers
ArraySum
DisplaySum
在组合语言之中,输入输出的工作常需要详细的编码才能实做出 .为
减少一些细节的部份,我们可以呼叫程序 清除萤幕,显示字 ,输入一个整
及显示一个整 :
Main
Clrscr ; 清除萤幕
PromptForIntegers
WriteString ; 显示字
ReadInt ; 输入整
ArraySum ; 计算整 和
DisplaySum
WriteString ; 显示字
WriteInt ; 显示整
第5章 程序 5-51
结构图 (Structure Chart)
下 的图型称之为结构图,用 描述程式的结构.其中使用到 络函 库
的程序,以阴影表示之:
根程式 (Stub Program)
在将程式实作出 之前,让我们先设计一个缩小版本(最小化)的程式,
称之为根程式(指程式将从此根发芽成长).它只包含空的(或是近乎空的)程序.
这个程式在组译后可以执 ,但实际上 会做出任何有用的事情:
TITLE Integer Summation Program (Sum1.asm)
; 这个程式由使用者输入多个整
; 并将之存到一个阵 之中,计算其总和
; 并在萤幕上显示其结果
INCLUDE Irvine32.inc
.code
main PROC
; 主程式控制程序
; 呼叫程序: Clrscr, PromptForIntegers,
; ArraySum, DisplaySum
exit
main ENDP
;-----------------------------------------------------
PromptForIntegers PROC
;
; 提示使用者一个整 阵 ,并将
5-52 Intel 组合语言
; 使用者的输入值填入阵 之中.
; 接收 : ESI 指向一个双字整 阵
; ECX = 阵 大小.
; 回传值: 回传
; 呼叫程序: ReadInt, WriteString
;-----------------------------------------------------
ret
PromptForIntegers ENDP
;-----------------------------------------------------
ArraySum PROC
;
; 计算一个32位元整 阵 的整 和.
; 接收 : ESI 指向阵 , ECX = 阵 大小
; 回传值: EAX = 阵 元素总和
;-----------------------------------------------------
ret
ArraySum ENDP
;-----------------------------------------------------
DisplaySum PROC
;
; 在萤幕上显示总和.
; 接收 : EAX = 总和
; 回传值: 回传
; 呼叫程序: WriteString, WriteInt
;-----------------------------------------------------
ret
DisplaySum ENDP
END main
存根模组程式可以让你有机会安排所有的程序呼叫,检视程序之间的依存
关系,有可能的话,在编写细节部份的程式码前,改善程式结构的设计.因此,
很清楚地,你应该为每一个程序加上注释, 明它的作用及所需要的 .
第5章 程序 5-53
5.6.1.1 整 加法程式实作
该是完成加法程式的时候 .在资 段宣告一个整 阵 ,使用一个符号
名当做阵 的大小:
IntegerCount = 3
array DWORD IntegerCount DUP( )
有二个字 要被用 当作萤幕上的提示:
prompt1 BYTE "Enter a signed integer: ", 0
prompt2 BYTE "The sum of the integers is: ", 0
在main程序中会清除萤幕,传递一个阵 指标给PromptForIntegers程
序,呼叫ArraySum,并呼叫DisplaySum:
call Clrscr
mov esi, OFFSET array
mov ecx, IntegerCount
call PromptForIntegers
call ArraySum
call DisplaySum
PromptFroIntegers呼叫WriteString 提示使用者输入一个整 .它接著
会呼叫ReadInt接收使用者的输入,并将整 存到ESI所指向的阵 之中.
回圈会让这些步骤执 多次.
ArraySum计算并回传阵 中所有整 的和.
DisplaySum会在萤幕上显示出一段讯息("The Sum of the integers is:")并
呼叫WriteInt 显示在EAX中的整 .
完整程式表
下 程式码是整 加法程式的完整程式码:
5-54 Intel 组合语言
TITLE Integer Summation Program (Sum1.asm)
; 这个程式由使用者输入多个整 , 并将之存到一个阵 之中,计算其总和
; 并在萤幕上显示其结果
INCLUDE Irvine32.inc
INCLUDE Irvine32.inc
IntegerCount = 3 ; 阵 大小
.data
prompt1 BYTE "Enter a signed integer: ",0
prompt2 BYTE "The sum of the integers is: ",0
array DWORD IntegerCount DUP( )
.code
main PROC
call Clrscr
mov esi, OFFSET array
mov ecx, IntegerCount
call PromptForIntegers
call ArraySum
call DisplaySum
exit
main ENDP
;-----------------------------------------------------
PromptForIntegers PROC
; 提示使用者一个整 阵 ,并将使用者的输入值填入阵 之中.
; 接收 : ESI 指向阵 , ECX = 阵 大小.
; 回传值: 回传
; 呼叫程序: ReadInt, WriteString
;-----------------------------------------------------
pushad ; 储存所有暂存器
mov edx, OFFSET prompt1 ; 命 提示的位址
L1:
call WriteString ; 显示字
call ReadInt ; 将整 进EAX中
call Crlf ; 换
mov [esi], eax ; 存到阵 之中
add esi, 4 ; 下一个整
loop L1
popad ; 原所有暂存器
ret
PromptForIntegers ENDP
第5章 程序 5-55
;-----------------------------------------------------
ArraySum PROC
;
; 计算一个32位元整 阵 的整 和.
; 接收 : ESI 指向阵
; ECX = 阵 元素个
; 回传值: EAX = 阵 元素总和
;-----------------------------------------------------
push esi ; 储存ESI及ECX
push ecx
mov eax,0 ; 将总和归
L1:
add eax,[esi] ; 将每一个整 加到总和之中
add esi,4 ; 指向下一个整
loop L1 ; 重 到阵 结束
pop ecx ; 原ESI及ECX的值
pop esi
ret ; 总和结果在EAX中
ArraySum ENDP
;-----------------------------------------------------
DisplaySum PROC
;
; 在萤幕上显示总和.
; 接收 : EAX = 总和
; 回传值: 回传
; 呼叫程序: WriteString, WriteInt
;-----------------------------------------------------
push edx
mov edx, OFFSET prompt2 ; 显示讯息
call WriteString
call WriteInt ; 显示EAX的值
call Crlf
pop edx
ret
DisplaySum ENDP
END main
5-56 Intel 组合语言
5.6.2 自我评
1. 将一个大的工作分成 个较小的部份的过程我们称之为 麼
2. 在加法程式设计之中,那些程序是 向自Irvine32函 库中.
3. 麼是Stub Program
4. (是非题)在加法程式(章节5.6.1.1)中的ArraySum程序直接 照一个
阵 名称变 .
5. 在加法程式(章节5.6.1.1)中的PromptForIntegers程序 ,对那一 做
修改后,可以让程式能够处 16位元的字元 展示之.
6. 画出在加法程式中,PromptForIntegers程序的 程图( 程图在章节5.5.4
中介绍过).
第5章 程序 5-57
5.7 本章摘要
5.7 Chapter Summary
在本章之中介绍 本书的 结函 库,它可以让你在组合语言的应用程式
中,处 输入及输出时 容 些.
在表5-1 出 Irvine32 结函 库中大部份的程序.最新版的程序会在
本书的网站中做定期的 新,你可以到那儿下载.
在章节5.3.3中的函 库测试程式,展示 一些在Irvine32函 库中输入输
出的函 .它会产生并显示出 出的 ,暂存器倾印及记忆体倾印.它以
同的格式显示整 ,并展示 字 的输入及输出.
执 时间堆叠是一个特别的阵 ,用 当作一个暂时的存放空间,储存位
址或资 .ESP暂存器会在堆叠中的某处,拥有一个32位元的偏移值.堆叠称
为LIFO结构(后进先出),因为最后存进堆叠中的值会最先被取出.PUSH运
算会将一笔资 复制到堆叠之中.POP运算会由堆叠之中移除一笔资 ,并将
它复制到一个暂存器或变 之中.堆叠经常用 保存程序的回传位址,程序
,区域变 及在程序内部所使用的暂存器.
PUSH指 会先减少堆叠指标的值,然后将 源运算元复制到堆叠之中.
POP指 会先复制堆叠之中由ESP所指向的内容,并将之复制到一个16或32
位元的目的运算元,然后减少ESP的值.
PUSHAD指 会将一般用途的32位元暂存器PUSH到堆叠之中,而PUSHA
指 也是一样的作用,只是它是用 对16位元的暂存器.POPAD指 会由堆叠
中POP资 到一个一般用途的32位元暂存器,而POPA指 也是一样的作用,
但是它用 POP到16位元的暂存器.
PUSHFD指 会PUSH一个32位元的EFLAGS暂存器到堆叠中,而POPFD
则会由堆叠中POP资 到EFLAGS.PUSHF及POPF的作用相同,但它们只适
用16位元的FLAGS暂存器.
5-58 Intel 组合语言
在章节5.4.2.5中介绍的RevString程式, 用堆叠 反转一个字 中的字
元顺序.
程序是在程式当中被命名的一段区块,宣告时使用PROC及ENDP指引
宣告.程序一定要以RET指 当作它的结尾.在章节5.5.1.2中的SumOf程
序,用 计算三个整 的和.CALL指 会藉由将程序的位址插入到指 指标暂
存器中 执 程序.当一个程序完成后,程序结尾处的RET指 (Return From
Procedure)会将处 器带回到程式之中,程序被呼叫的位置上.巢 程序呼叫,
是指在一个被呼叫的程序之中,在其完成并回传之前,再呼叫另一个程序.
在预设的情形下,程式标签(跟著一个冒号)会被当成是区域标签,其有
效区为其所在的程序之中.而跟著二个冒号的程式标签则是全域标签,其有效
区域为其所在的程式档案中.
章节5.5.3之中的ArraySum程序,会计算并回传一个阵 中所有 字的总
和.
伴随著PROC指引的USES运算子可以让你 出该程序所使用的暂存器.
组译器会在程序的开头及结尾处产生一些程式码,将这些暂存器在程序开始
时,PUSH到堆叠之中,并在结束时POP出 .
任何大小的程式 应该藉由程式 明 小心的设计.标准的方法是使用功
能性分解(由上而上的设计方式) 将程式分解为 个程序.首先,先确认程
序之间的 结关系及顺序,然后再填写程序的细节内容.
第5章 程序 5-59
5.8 程式设计 习
5.8 Programming Execises
1. 改变文字颜色
使用书中的 结函 库 撰写一个程式,让一个字 以四种 同的颜色显
示在萤幕上.
2. 整 阵 输入
撰写一个程式,使用回圈让使用者输入十个32位元的整 ,并将之存於在
一个阵 之中,然后再将它们显示在萤幕上.
3. 简单的加法(1)
撰写一个程式,先清除萤幕,然后将游标定位在萤幕的正中央.提示使用
者输入二个整 ,加总它们并显示其总和於萤幕上.
4. 简单的加法(2)
使用前一个 习的程式当作开端.使用回圈,让这个新的程式可以重复同
样的步骤三次,在每次回圈重复时清除萤幕.
5.
撰写一个可以产生50个介於+20到-20之间的 ,并将之显示在萤幕上.
6. 随机字
写一个程式,产生并显示出二十个随机字 ,每一个字 包含十个大写的
字母{A…Z}.
5-60 Intel 组合语言
7. 随机萤幕位置
撰写一个程式,将一个字元显示在萤幕上一百个随机的位置.挑战:如果
可以的话,让字元以10到300ms之间的时间间隔,随机显示.
8. 色彩阵
写一个程式使用所有可能的文字颜色和底色(16×16=256)的组合, 显示
一个单独的字元.颜色的代号是从0到15,因此你可以使用一个巢 回圈 产
生所有可能的组合方式.
·上一篇:华高科技Web
·下一篇:用户指南

文件类型:PDF/Adobe Acrobat 文件大小:字节