第八章是一个非常重要的章节,讲述的是实模式下对硬件的访问(这一节主要讲的是硬盘),还有用户程序重定位的问题。现在整理出来刚好能和保护模式下的用户程序定位作一个对比。
★PART1:用户程序的重定位,硬盘的访问
1. 分段、段的汇编地址和段内汇编地址
NASM编译器使用汇编指令“SECTION”或者“SEGMENT”来定义段。他的一般格式是SECTION 段名称或者SEGMENT段名称(段名称不能重复),另外NASM对段没有数量的限制,一个程序可以有很多的代码段和数据段。Intel处理器要求段在内存中的其是物理地址起码是16字节对齐的,而NASM提供了段的修饰符align,使每一个段可以16字节对齐或者32字节对齐,比如
所谓段的汇编地址其实就是段内第一个元素(数据,指令)的汇编地址,16字节对齐的意思是所有段首的汇编地址都要可以被16整除,如果存在一个段要求16字节对齐,而这个段的前一个段长度不够使当前段不能16字节对齐,那么编译器会自动将前一个段补0来使这一个段满足16字节对齐。
NASM编译器提供以下形式section.段名称.start来获得段的汇编地址,比如:
另外段还可以加一个vsart修饰符,因为在NASM编译器中,即使你定义了一个段,段的汇编地址就是段内第一个元素的汇编地址,但是在引用某个标号的时候(包括section.段名称.start),这个标号的汇编地址还是从整个程序的开头开始计算的,而不是对段首的偏移。不过再加了vsart=0的时候,段内所有标号的地址都是相对于当前段首的偏移了(当然也可以设定为其他数值,标号的偏移值是在这个值的基础上加上与段首的偏移地址。)
2. 用户程序头部
加载一个用户程序需要一个加载器(在实模式下),而加载器是不知道用户程序里面具体的结构和功能的,一个程序想要运行,那么这个程序就要满足运行环境的一些约定俗成的条件,也就是程序哪些部分要怎么写是固定的,现在我们在MBR加载一个程序也是一样的,只要用户程序在某些部分满足一些条件,我们的加载器就可以识别并加载它。
一般来说,头部在源程序需要以一个段的形式存在,比如可以这样写:
一般地,用户程序头部最起码要包含以下信息:
①:用户程序的尺寸,以字节位单位的大小。加载器将根据这个信息来决定读取多少个逻辑扇区(假如程序在磁盘上都是连续存放的)。
②:应用程序的入口点(Entry Point),包括段地址和偏移地址。用户程序需要给出自身第一条指令的位置,让加载器加载程序以后可以把控制权交给用户程序。
③:段的重定位表,用户程序可能不止一个段,而实际上每个用户程序编译后各个段的位置只是相对于程序的最开始的位置,加载器一定要根据程序加载的地方重新设定各个段的位置!段的数量要预先给定。在实模式下,段的 重定位就是重新设定每个段基地址(16位的),所以每个段都必须16字节对齐(因为可以加载在1MB内存的任何一个64KB的段,而我们无法用16字节表示1MB的位置,所以只能用双字来设定位置,最后要设定为16位的段地址),才能给出正确的段地址。
3. 外围设备和其接口
在加载器根据跟定的用户程序在内存的位置给重新设定用户程序的入口点和给段重定位之前,需要把用户程序加载到相应内存位置,而用户程序一般是放在硬盘上的,所以加载器首先根据用户程序在硬盘上的逻辑扇区号来加载程序。
加载器访问硬盘,说白了就是在访问外围设备(Peripheral Equipment)。设备可以分两种,一种是输入设备,比如键盘,鼠标,麦克风,摄像头等;另一种是输出设备,比如显示器,打印机,扬声器等。输入设备和输出设备统称为输入输出设备(Input/Output,I/O)。
处理器通过I/O接口与所有设备连接,处理器用了两个方法与外围设备打交道。
第一个就是采用了总线技术(Bus),总线就是一排连接所有设备的线路,每个连接到这个线路上的器件都必须拥有电子开关。以至于他们随时都能够同这排电线连接,或者从这排电线上断开。
第二个就是采用输入输出控制设备集中器(I/O Controller Hub,ICH)芯片,该芯片是连接不同的总线,并协调各个I/O接口对处理器的访问,在个人计算机上,这块芯片就是耳熟能详的南桥。
如图,处理器通过局部总线连接到ICH内部的处理器接口电路,然后在ICH的内部,又通过总线与各个I/O相连。在ICH的内部,集成了一些常规的外围设备接口,如USB,PATA(IDE),SATA,老式的总线接口(LPC)、时钟等。每个设备都有自己的I/O接口电路同ICH相连。为了方便,主板上都有这些I/O接口的插槽,每个设备的I/O电路都设计成插卡式,可以方便插拔。
ICH还对PCI(PCI Express)总线的支持,这条总线向外延伸,连接这主板上的若干个拓展槽,比如显卡就可以插在PCI上,然后把显示器接在显卡上。除了局部总线和PCI Express总线,每个I/O接口卡可能不止连接一个设备,这些线路涉及复用和仲裁问题,所以他们自己有一套独立的总线体系,称为通讯总线或者设备总线,比如USB总线和SATA总线。
当处理器想访问某个设备的时候,ICH会接到通知,然后负责提供传输通道和其他辅助支持,并不允许其他设备和总线连接,反过来,某个设备想要连接处理器,也是一样的。
4. I/O接口访问和端口访问
处理器是通过端口(Port)来和外围设备打交道的。本质上,端口就是一些寄存器,类似于处理器内部的寄存器,不同之处仅在于,这些寄存器位于I/O接口电路中。端口是处理器和外围设备通过I/O交流的窗口,每一个I/O接口可能拥有很多个端口,分别用于不同的目的。端口在不同的计算机系统有着不同的实现方式,在一些计算机系统中,端口号是映射到内存地址空间的,比如0x00000~0xE0000是真实的物理内存地址,而0xE0001~0xFFFFF是从很多I/O接口那里映射过来的,当访问这部分地址的时候,实际上是在访问I/O接口。
而在另一些计算机系统中,端口是独立编制的,不和内存发生关系,在这种计算机中,处理器的地址线及连接内存,也连接着每一个I/O接口,但是,处理器还有一个特殊的引脚M/IO#,在这里,”#”表示低电平有效,也就是说,处理器访问内存的时候,他会让M/IO#引脚呈现高电平,这个时候与内存相关的电路就会被打开,相反,如果处理器访问I/O接口,那么M/IO#引脚呈现低电,内存电路会被禁止。与此同时,处理器发出的地址和M/IO#信号一起用来打开某个I/O接口(该I/O接口分配的端口号和处理器地址吻合的话)。
Intel处理器,早期是独立编制的,现在既有内存映射的,也有独立编制的,书上是以独立编制为例子的。
在独立编制的系统中,存在65536个端口(端口号从0~65535),因为是独立编制,所以不能使用mov指令访问端口,只能通过in和out指令来进行读和写。端口可以是8位或者16位的也可以是32位的。
in指令是从端口读,他有两种形式:
out指令是对端口写,他也有两种形式和in指令是对称的
5. 硬盘的访问(重点)
PATA/SATA接口中的数据端口是16位的,读写硬盘的基本单位是字扇区(硬盘是典型的块设备,不能仅读写扇区中的某一个字节)。读写硬盘可以有两种方式,一种是CHS模式(即向硬盘控制器分别发送磁头号,柱面号和扇区号),另一种就是LBA模式,所谓LBA模式,其实就是将磁头号,柱面号和扇区号统一编号,LBA的计算方法是:
LBA号=C*磁头总数*每道扇区数+H*每道扇区数+(S-1)
早期LBA采用28个比特来表示逻辑扇区号(LBA28),从逻辑扇区(0x0000000~0xFFFFFFF),一共可以表示228个比特,每个扇区可以有512个字节,所以LBA28可以管理128GB的硬盘。现在普遍采用的是LBA48,采用48个比特来表示逻辑扇区号,这样一来就可以表示131072TB的硬盘了。
教材采用的是LBA28访问硬盘(因为讲的是实模式)。
第一步:设置要读取的扇区数量(操作端口0x1f2),这个数值要写入0x1f2端口,这是个8位的端口,因此每次最多读写255个扇区。
如果对0x1f2端口写入0,那么就表示要读取256个扇区,每读取一个扇区,这个数值就会减1,如果读写的过程中发生错误,那么这个端口就包含这尚未读取的硬盘数。
第二步:设置起始LBA扇区号(操作端口0x1f3-0x1f7),扇区的读写是连续的,所以只要给出第一个扇区的编号就可以读取所有的扇区了,28个扇区号将会被分成4段(每个段8位),分别写入0x1f3-0x1f6端口,其中0x1f3端口存放的是0-7位,
0x1f4端口存放8-15位,0x1f5存放16-23位,0x1f6存放24-27位,最后,0x1f6高4位用来指示是主盘或者是从盘,以及是采用CHS模式还是LBA模式。
第三步:向端口0x1f7写入0x20,请求端口读(写入0x30就是请求端口写)
第四步:等待读写操作完成(操作端口0x1f7)。0x1f7既是命令端口,又是状态端口。在硬盘内部操作期间,它会将0x1f7端口的第7位置1,表明硬盘在忙,一旦硬盘准备好了,它再将这个位清零,同时将第三位置1,表明这个时候主机可以向硬盘发送数据或者从硬盘接受数据了。我们可以使用条件转移指令完成这个动作,代码如下:
第五步:连续取出数据(操作端口0x1f0)。0x1f0是硬盘的数据接口,而且这个端口还是16位的,一旦硬盘准备就绪,就可以连续地从这个接口写入或者读取数据了。代码例子如下:
最后,0x1f1是一个错误代码寄存器,里面可以读取硬盘读写错误的原因。
6. 加载用户程序以及用户程序重定位,加载器到用户程序的跳转(重点)
加载器首先是在硬盘上读取用户文件(程序),因为我们肯定是要事先知道用户头部的(总长度,所有段地址等),所以要预先读取一个扇区,又因为每个扇区是512个字节,但是用户程序可能很大,而在实模式下,每个段的大小最大是64KB,所以每加载一个扇区,都重新设定段(每个段地址只要往后移动0x20就可以了),这样就能把程序加载到内存中了(当然用户程序也不能太大,但是在实模式下运行的程序一般都是很小的)。
当然了,我们预先读取了一个扇区,所以我们向上取整(当出现余数的时候-1)。所以我们可以使用以下代码读取程序。一定要注意,在每次读取一个扇区的时候,si要跟着变化(+1)
因为在我们自己写的系统中,我们可以强制规定应用程序的头部应该包含什么东西,现在我们可以规定头部是这样的(参照书上的)。
我们可以看到用户头部有程序的总长度(用于主引导程序确定要读取多少个扇区),程序的入口点(包括偏移位置,用于决定主引导程序跳进程序后究竟在哪执行代码),段的总数和所有段地址(相对地址,在编译期间已经确定,需要主引导程序重定位)。
接下来我们就要开始段的重定位了,所谓段的重定位,其实这个名词看上去很高大上,而事实上就是在用户程序头部的所有段地址都加上加载位置,并且把这个加载位置转化为段的偏移地址罢了,而我们已经规定了用户程序在内存加载的地址是phy_base,接下来我们直接读取头部的内容,然后首先给入口点重定位,然后就是所有段地址重定位。
这里我们可以看到,我们使用了adc命令,是带进位的加法,执行adc命令的时候,最终结果会加上CF的值,这和第七章的那个最后的那个1-1000的相加的方法是一样的,注意的是,因为我们之前读扇区的时候,故意把读入的数据存放位置也按扇区512字节来划分了(改变了ds的值),在最后一定要记得把ds的值还原为在用户头部的段地址。这里有很多易错的地方,第一个就是bx是偏移量,而不是用内存寻址找到的量(粗心就会放错),另外就是书上用的是两次左移命令(shr和ror)使物理地址变偏移地址的,个人觉得不是很必要,直接用32位除法就可以了,用bx作为除数的时候,bx的原来值一定要注意要保留,不然就会出错!
最后我们可以使用上述指令直接跳到用户程序执行了,因为这个时候ds指示的是用户程序头部,而0x04刚好是偏移地址,0x06刚好是段地址,用远转移指令直接跳转,会把cs的值设为0x06的值,IP的值设为0x04的值。
★PART2:执行用户程序
1. 过程调用
进入用户程序之后,我们首先要做的,就是马上把ds设置为自己的程序段(可以es用来一直指向头部,或者也可以再每一个代码段末尾设置用户程序头部,我们的例子直接用es了),然后就可以进行用户程序所设定的任务了,当然我们的用户程序的的任务比较简单,就是显示一个字符串。
教材上在这一章介绍了call和jmp的全部用法,本来我是想把所有的指令都统一到一起讲的,但是想到call和jmp这两个指令在这里的作用确实太大了,所以直接拿到这里记录就好了。
8086处理器无条件转移指令jmp:
相对短转移jmp short imm8:
操作数是相对于目标位置的偏移量,imm8指示一个8位的有符号数(-127,128),所以这是一个段内转移指令,执行指令后,cs不变,ip会加上偏移地址成为新的偏移地址
(注意,不管是call指令还是jmp指令,因为每执行一次指令,ip的值都会自动到下一条指令的位置上,所以这个偏移量的大小,实际上是ip当前位置的汇编地址(对于其段首)减去目标地址的汇编地址(对于其段首),而不是执行转移前ip的地址
比如:
Goal: jmp near Goal
xor dx,dx
ip的值在执行jmp时,已经是xor的汇编地址的(相对于其段地址),这个时候偏移地址应该是-3(因为jmp near Goal长度是3个字节长度)
)。
16位相对近转移指令jmp near imm16
和相对短转移指令类似,只是操作数可以是16位的了,在没有指示near和short,编译器会根据偏移量长度来确定是near还是short(-127,128是short,大于这个范围是near)。执行指令后,cs不变,ip会加上偏移地址成为新的偏移地址。
16位间接绝对近转移指令jmp near r16\m16
和16位相对近转移指令类似,只是操作数不再是一个16位的立即数了,而是一个16位的通用寄存器或者内存单元,near可以省略,执行指令后,cs不变,ip会被内存\寄存器指示的偏移地址取代。
16位直接绝对远转移指令jmp far imm16:imm16
其中冒号左边的立即数是段地址,冒号右边的立即数是偏移地址,在执行这个指令后,IP会被替换成冒号左边的的值,cs会变成冒号右边的值。far不可以省略。
16位间接绝对远转移指令jmp far m32
可以指定一个32位的内存空间,前16位为偏移地址,后16位位段地址,far不可以省略,在执行这个指令后,IP会被替换成内存指示的低16位,cs会变成内存指示的高16位。
(内存寻址可以是任何一种内存寻址模式(直接寻址,基址寻址,变址寻址,基址变址寻址))。
8086处理器过程调用指令call:
16位相对近调用call imm16
16位相对近转移调用指令是一个3字节的指令,和16位相对近转移指令类似,他的操作数也是一个偏移量,指令执行后,cs不变,ip会加上偏移地址成为新的偏移地址。
16位绝对近调用call r16\m16
和16位间接绝对近转移指令类似,操作数也是一个16位的寄存器或者内存单元,执行指令后,cs不变,ip会被内存\寄存器指示的偏移地址取代。
16位直接绝对远调用call far imm16:imm16
和16位直接绝对远转移指令类似,操作数是两个16位数,冒号左边的立即数是段地址,冒号右边的立即数是偏移地址,在执行这个指令后,IP会被替换成冒号左边的的值,cs会变成冒号右边的值。far不可以省略。
16位相对绝对远调用call far m32
和16位间接绝对远转移指令类似,操作数是一个32位的内存空间,前16位为偏移地址,后16位位段地址,far不可以省略,在执行这个指令后,IP会被替换成内存指示的低16位,cs会变成内存指示的高16位。
call指令和jmp指令不一样的地方在于,call指令可以用ret(配对近调用)和retf(配对远调用),ret执行的时候,处理器会从栈中弹出一个字的指令到ip中并替换ip的值,retf会弹出两个字,分别是段地址和偏移地址分别替代ip的值和cs的值。
事实上所有的call指令都会有压栈动作,如果是近转移,那么就把ip的值压栈,如果是远转移,那么就把ip的值和cs的值压栈。而事实上,我们如果不执行call指令,直接用push指令也可以产生相同的效果,ret/retf只是负责弹出内容到相应的地方而已。(也就是说,call要和ret或者retf配对,ret和retf不一定要和call配对),call指令不对任何标志位产生影响,ret/retf也不会对任何标志位产生影响。
所以现在我们就调用我们在代码段2的显示字符串的代码,为了内存利用最大化,可以利用了retf的特性,把代码段2的地址和代码段2的第一条指令的偏移地址压栈,然后直接用retf跳转。
2. 对光标的控制
在文本模式下,光标是一个很重要的东西,光标可以指示下一个字符的写入位置,在第九章对光标的控制用更快的方法就是利用中断就可以自动将光标放到对应的位置,在这里我们直接用最简单的方法来实现一些光标的简单的功能就可以了,在教材上的这一章上,光标是不能动的,所以指示清屏换行和回车这些特殊符号就可以了。
显卡的操作很复杂,为了不过多地占用主机的I/O空间,很多寄存器只能通过索引寄存器间接访问,索引寄存器的端口号是0x3d4,可以向他写入一个值来只是内部的某个寄存器,比如写入0x0e就可以获取光标位置的高8位,写入0x0f就可以获得光标位置的低8位。所以我们可以直接这么写获取光标位置到ax寄存器:
★PART3:本章的所有代码:
1. 主引导程序
;-----------------------实模式下主引导扇区代码----------------------------------
SECTION code_mbr_start align= vstart=0x7c00 ;一定不要忘记要给vstart=0x7c00
start:
app_lba_start equ ;声明常数(用户程序起始逻辑扇区号) mov ax,
mov ss,ax
mov sp,ax mov ax,[cs:phy_base]
mov dx,[cs:phy_base+0x02]
mov bx,
div bx
mov ds,ax ;让ds指向内存段
mov es,ax ;因为等下用户程序要用到es,顺便一起设置了 xor di,di ;逻辑扇区的16-27位
mov si,app_lba_start ;逻辑扇区的0-15位
xor bx,bx
call read_harddisk_0 mov ax,[0x00] ;得到总长度
mov dx,[0x02]
mov bx,0x200
div bx ;ax得到占用多少个扇区 cmp dx,
jne read_last_content ;如果不等于0,那就不需要减去1了
dec ax read_last_content: ;读取剩余的扇区
cmp ax, ;等于0就不用再读取了
je begin push ds ;先存一下指向用户程序头部的ds mov cx,ax
xor bx,bx
@loop1:
mov ax,ds
add ax,0x20 ;因为每次只读一个扇区,所以要把段地址每次都移动512个字节
mov ds,ax
inc si ;读下一个扇区,一定要记得+1!
call read_harddisk_0
loop @loop1 pop ds begin:
;然后开始给所有的段重定位
;先回填用户程序起始代代码段
mov bx,0x06
call realloc_segement mov cx,[0x0a]
mov bx,0x0c ;注意bx是偏移地址
realloc:
call realloc_segement
add bx,
loop realloc jmp far [0x04] ;远转移指令,直接读取32个字,前16位是偏移地址,后16位是段地址 ;-------------------------------------------------------------------------------
;---------------------------------函数部分--------------------------------------
;-------------------------------------------------------------------------------
read_harddisk_0:
push ax
push bx
push cx
push dx mov dx,0x1f2
mov al,0x01
out dx,al ;请求读一个硬盘 mov ax,si ;0~7位,端口0x1f3
inc dx
out dx,al mov al,ah ;8~15位,端口0x1f4
inc dx
out dx,al inc dx ;16-23位,端口0x1f5
mov ax,di
out dx,al inc dx
mov al,0xe0 ;LBA28模式,主盘
and ah,0x0f ;清掉高4位,24-27位,端口0x1f6
or al,ah
out dx,al inc dx
mov al,0x20 ;读命令,端口0x1f7(命令端口)
out dx,al .wait:
in al,dx
and al,0x88
cmp al,0x08
jnz .wait mov cx,
mov dx,0x1f0 .read:
in ax,dx
mov [bx],ax
add bx,
loop .read pop dx
pop cx
pop bx
pop ax ret
;-------------------------------------------------------------------------------
realloc_segement:
push ax mov ax,[bx] ;注意地址是32位的!
mov dx,[bx+0x02] push bx
add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02] ;直接用除法指令就好了不需要用左移那么复杂
mov bx,
div bx pop bx
mov [bx],ax pop ax ret
;-------------------------------------------------------------------------------
;----------------------------------数据区---------------------------------------
;-------------------------------------------------------------------------------
phy_base dd 0x10000 ;用户程序被加载的物理起始地址
;-------------------------------------------------------------------------------
tail:
times -($-$$) db
dw 0xaa55
2. 用户程序
;==============================用户程序=======================================
SECTION header vstart= ;定义用户程序头部段
program_length: dd program_end ;程序总长度[0x00]
program_entry_point: dw start ;程序偏移地址[0x04]
dd section.code_1.start ;程序入口点[0x06]
realloc_tbl_len: dw (header_end-code1_segment)/ ;[0x0a] code1_segment: dd section.code_1.start ;[0x0c]
code2_segment: dd section.code_2.start ;[0x10]
data1_segment: dd section.data_1.start ;[0x14]
data2_segment: dd section.data_2.start ;[0x18]
stack_segment: dd section.stack.start ;[0x1c]
header_end:
next_ip: dw
;===============================================================================
SECTION code_1 align= vstart= ;定义代码段1(16字节对齐)
;------------------------------------------------------------------------------
start:
mov ax,[es:data1_segment] ;立马将ds设置到自己程序的数据段中
mov ds,ax mov ax,[stack_segment]
mov ss,ax
mov sp,stack_end
call_1:
push word[es:code2_segment]
mov ax,put_string
push ax
mov word[es:next_ip],call_2
mov bx,msg0 retf
call_2:
mov ax,[es:data2_segment]
mov ds,ax push word[es:code2_segment]
mov ax,put_string
push ax
mov word[es:next_ip],stop
mov bx,msg1 retf
stop:
cli ;关中断
hlt ;停机 ;===============================================================================
SECTION code_2 align= vstart= ;定义代码段2(16字节对齐)
put_string:
mov cl,[bx]
cmp cl,
je _exit ;遇到结束符立马退出打印
call put_char
inc bx
jmp put_string _exit:
push word[es:code1_segment]
mov ax,[es:next_ip]
push ax
retf put_char:
push ax
push bx
push cx
push dx
push ds
push es mov dx,0x3d4
mov al,0x0e
out dx,al ;写入0x0e(索引值14),表示需要访问光标的高八位
mov dx,0x3d5
in al,dx
mov ah,al ;将al的值先放入ah
mov dx,0x3d4
mov al,0x0f ;写入0x0f(索引值15),表示需要访问光标的低八位
out dx,al
mov dx,0x3d5
in al,dx ;现在ax的值就是光标当前的位置了 .judge:
cmp cl,0x0d ;判断是不是回车符
je .set_0x0d
cmp cl,0x0a
je .set_0x0a
.print:
mov bx,ax ;让bx指向当前光标的位置
mov ax,0xb800
mov ds,ax shl bx, ;注意字符显示的位置是光标位置的两倍
mov [bx],cl
mov byte[bx+],0x07 ;注意光标一定是要比字符移动要前,
add bx, ;所以只用关心光标是否需要滚屏就可以了
shr bx,
cmp bx,
jge .roll_screen
jmp .set_cursor
.set_0x0d:
mov bx,
div bl
mul bl
mov bx,ax ;偏移地址都放在bx上
jmp .set_cursor
.set_0x0a:
add ax,
mov bx,ax
cmp bx,
jge .roll_screen ;超出2000字符滚屏
jmp .set_cursor
.roll_screen:
mov ax,0xb800
mov es,ax
mov ax,0xb800
mov ds,ax
mov di,
mov si,0xa0 cld
mov cx,
rep movsw mov bx,
mov cx,
.cls:
mov word[es:bx],0x0720 ;填充空格
add bx,
loop .cls
mov bx, ;设定光标最终的位置 .set_cursor:
mov dx,0x3d4
mov al,0x0f ;低8位
out dx,al
mov al,bl
mov dx,0x3d5
out dx,al
mov dx,0x3d4
mov al,0x0e ;高8位
out dx,al
mov al,bh
mov dx,0x3d5
out dx,al .out:
pop es
pop ds
pop dx
pop cx
pop bx
pop ax ret ;===============================================================================
SECTION data_1 align= vstart= msg0 db 'My name is Philip. The demo is running>>>>>>>>>>>',0x0d,0x0a,0x0d,0x0a
db 'Back at SourceForge and in intensive development! ',0x0d,0x0a
db 'Get the current versions from http://www.nasm.us/.',0x0d,0x0a
db 0x0d,0x0a,0x0d,0x0a
db ' Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
db ' xor dx,dx',0x0d,0x0a
db ' xor ax,ax',0x0d,0x0a
db ' xor cx,cx',0x0d,0x0a
db ' @@:',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' add ax,cx',0x0d,0x0a
db ' adc dx,0',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' cmp cx,1000',0x0d,0x0a
db ' jle @@',0x0d,0x0a
db ' ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
db
;===============================================================================
SECTION data_2 align= vstart= msg1 db ' The above contents is from the textbook <<x86>> '
db '2016-03-02'
db ;===============================================================================
SECTION stack align= vstart= times db stack_end: ;===============================================================================
SECTION trail align=
program_end: