《x86汇编语言:从实模式到保护模式》学习笔记


实模式内存

  • 实模式下内存会限制位1MB(FFFFF)
    • 00000~9FFFF:DRAM
    • A0000~EFFFF:设备存储
    • F0000~FFFFF:Rom bios(Basic Input Output System)
    • 00000~0FFFF:主引导扇区程序(加载器)及其所用的栈
    • 10000~9FFFF:用户程序可用的区域
  • 0x7C00:引导程序加载地址
  • 0xb800:(0xB8000-0xBFFFF)位于设备存储区中显存在内存中的映射地址
  • 逻辑地址和物理地址
    • 00000~FFFFF中最低位字节位0时,就可以作为段基地址
    • 物理地址=段基地址>>4 + 偏移地址(4个字节)
    • 每个段的大小最大位64K,即65536个字节(偏移地址4个字节表示最大值)

寄存器

通用寄存器

  • AH&AL=AX(accumulator):累加寄存器,常用于运算;在乘除等指令中指定用来存放操作数,另外,所有的I/O指令都使用这一寄存器与外界设备传送数据.
  • BH&BL=BX(base):基址寄存器,常用于地址索引,用来提供数据访问,使用bx提供偏移地址时,段寄存器默认使用(指定使用需要加段超越前缀)ds提供段地址(bp默认访问栈段,bx默认访问数据段)
  • CH&CL=CX(count):计数寄存器,常用于计数;常用于保存计算值,如在移位指令,循环(loop)和串处理指令中用作隐含的计数器.
  • DH&DL=DX(data):数据寄存器,用来和外设之间进行数据传输
  • BP(Base Pointer)基址寄存器,用来提供访问栈段,使用bx提供偏移地址时,段寄存器默认使用ss提供段地址(bp默认访问栈段,bx默认访问数据段)
  • SP(Stack Pointer):堆栈指针,与SS配合使用,可指向目前的堆栈位置;
  • SI(Source Index):源索引寄存器(或变址寄存器)(用法见movsb),si提供偏移地址时,段寄存器默认使用ds提供段地址
  • DI(Destination Index):目标索引寄存器(或变址寄存器)(用法见movsb),di提供偏移地址时,段寄存器默认使用ds提供段地址

指针寄存器

  • 指令指针IP(Instruction Pointer):指令指针IP是一个16位专用寄存器,它指向当前需要取出的指令字节,当BIU从内存中取出一个指令字节后,IP就自动加1,指向下一个指令字节。注意,IP指向的是指令地址的段内地址偏移量,又称偏移地址(Offset Address)或有效地址(EA,Effective Address)。
  • BP 为基指针(Base Pointer) 寄存器,用它可直接存取堆栈中的数据;
  • SP 为堆栈指针(Stack Pointer) 寄存器,用它只可访问栈顶。(ss+sp=栈地址)【压栈操作:高地址(栈底)->低地址(栈顶)(8086每次压栈和出栈都是16字节)】

段寄存器

两个段寄存器之间不能直接传送数据,需要借助通用寄存器

  • CS: 代码段寄存器(Code Segment Register) ,其值为代码段的段值;
  • DS : 数据段寄存器(Data Segment Register) ,其值为数据段的段值;段内访问地址默认位ds寄存器(不指定的情况下)(段内地址位偏移地址)
  • ES: 附加段寄存器(Extra Segment Register) ,其值为附加数据段的段值;
  • SS: 堆栈段寄存器(Stack Segment Register) ,其值为堆栈段的段值;
  • FS: 附加段寄存器(Extra Segment Register) ,其值为附加数据段的段值;
  • GS: 附加段寄存器(Extra Segment Register) ,其值为附加数据段的段值。

标志寄存器

  • FLAGS寄存器
    • bit0(CF):进位标志,当一个算术在结果最高位产生进位或者借位时,标志为1,否则为0
      • 若al = 1000 0000则 add al, al CF = 1
    • bit2(PF):奇偶标志,当算术操作结果在低8位中有偶数个“1”时,此标志位1,反之则为0
    • bit4(AF):辅助进位标志,当一个算术操作在结果的位3(bit3)产生进位或者借位时,此为1,反则0
    • bit6(ZF):零标志,用来反映运算结果是否为0。如果运算结果为0,则其值为1,否则其值为0。在判断运算结果是否为0时,可使用此标志位。
    • bit7(SF):符号位,用来反映运算结果的符号位,它与运算结果的最高位相同。在微机系统中,有符号数采用补码表示法,所以,SF也就反映运算结果的正负号。运算结果为正数时,SF的值为0,否则其值为1。
    • bit8(TF):跟踪标志。该标志可用于程序调试。TF标志没有专门的指令来设置或清楚。如果TF=1,则CPU处于单步执行指令的工作方式,此时每执行完一条指令,就显示CPU内各个寄存器的当前值及CPU将要执行的下一条指令。如果TF=0,则处于连续工作模式。
    • bit9(IF):中断标志,中断允许标志IF位用来决定CPU是否响应CPU外部的可屏蔽中断发出的中断请求。但不管该标志为何值,CPU都必须响应CPU外部的不可屏蔽中断所发出的中断请求,以及CPU内部产生的中断请求。当IF=1时,CPU可以响应CPU外部的可屏蔽中断发出的中断请求;当IF=0时,CPU不响应CPU外部的可屏蔽中断发出的中断请求。
    • bit10(DF):方向标志,用来决定在串操作指令执行时有关指针寄存器发生调整的方向。
    • bit11(OF):溢出标志,对任何一个算术,假定算术为有符号运算,结果超出目标位置所能容纳最大正数或者最小负数时,此为1,反则0

可以提供偏移地址的寄存器

  • 可用BX,SI,DI,BP,其他都为非法
    • mov [ax], dl非法指令
    • mov [dx], bl非法指令
  • 基址变址组合
    • bx+si, bx+di, bp+si, bp+di
    • 非法例子
      • bx+ax
      • ax+cs

文本模式颜色表(80 X 25)

属性:KRGB IRGB(前4为背景色,后4为前景色)

R G B 背景色(K=0不闪烁,K=1闪烁) 前景色I=0 前景色I=1
0 0 0
0 0 1 浅蓝
0 1 0 绿 绿 浅绿
0 1 1 浅青
1 0 0 浅红
1 0 1 品(洋)红 品(洋)红 浅品(洋)红
1 1 0
1 1 1 亮白

指令

$和$$

  • nasm特有
    • $:当前行地址
    • $$:当前所在段起始地址

div(无符号除法)

  • 8位(寄存器或者8位操作数的内存地址)

    • 除数在寄存器AX中
    • 相除后:商在AL中,余数在AH中
    • div bh
    • div byte [0x2002]
  • 16位(寄存器或者16位操作数的内存地址)

    • 除数是32位的,低16位在AX中,高16位在DX中
    • 相除后:商在AX中,余数在DX中
    • div bx
    • div word [0x2002]
  • 32位(寄存器或者32位操作数的内存地址)

    • 除数是64位的,低32位在EAX中,高32位在EDX中
    • 相除后:商在EAX中,余数在EDX中
    • div ebx
    • div dword [0x2002]
  • 64位(寄存器或者64位操作数的内存地址)

    • 除数是128位的,低32位在RAX中,高64位在RDX中
    • 相除后:商在RAX中,余数在RDX中
    • div rbx
    • div qword [0x2002]

idiv(有符号除法)

  • 寄存器规则同div
  • 正负:如果被除数和除数符号相同,商为正数,否则商为负则为负数
  • 余数的符号始终和被除数相同

mul(无符号乘法)

  • 8位寄存器或者8位操作数的内存地址)
    • 被乘数是8位的,在寄存器AL中
    • 相乘后,乘积是16位的,在寄存器AX中
    • mul bh
    • mul byte [0x2002]
  • 16位(寄存器或者16位操作数的内存地址)
    • 被乘数是16位的,在寄存器AX中
    • 相除后:乘积是32位的,低16位在寄存器AX中,高16位在寄存器DX中
    • mul bx
    • mul word [0x2002]
  • 32位(寄存器或者32位操作数的内存地址)
    • 被乘数是32位的,在寄存器AX中
    • 相除后:乘积是64位的,低32位在寄存器EAX中,高32位在寄存器EDX中
    • mul ebx
    • mul dword [0x2002]
  • 64位(寄存器或者16位操作数的内存地址)
    • 被乘数是64位的,在寄存器RAX中
    • 相除后:乘积是128位的,低64位在寄存器RAX中,高64位在寄存器RDX中
    • mul rbx
    • mul qword [0x2002]

imul(无符号乘法)

add, adc

  • add:相加
  • adc:进位相加,受进位标志CF影响

xor

  • 异或指令
  • 可用于寄存器清零xor ax, ax,等价于mov ax 0,用异或操作的都是寄存器,速度会更快

neg

  • 求补码(取反) neg dx=> dx * -1
    • -2=>2, 3=>-3

shr,ror,shl,rol

  • shr:右移
  • ror:循环右移
  • shl:左移
  • rol:循环左移

near,for

  • near:绝对间近跳转,寻址范围为64K, jmp near table [近距离跳转]

dec, inc

  • dec:递减,i--
  • inc:递增,i++

rep,loop重复指令(cx会自己递减)

  • rep次数由cx寄存器决定(为0则停止)
    • rep movsw
  • loop机器码E2,8位相对偏移量,也就意味着,跳转位置不能太远,循环次数也是由cx确定,为0则中断循环

movsb, movsw

  • 数据传输指令,只会传送一次,一般辅助循环指令使用
  • 使用规则
    • DS:SI:原始数据串的段地址:偏移地址,si会自动改变值(根据方向递增或递减)
    • ES:DI:目标位置的段地址:偏移地址,di会自动改变值(根据方向递增或递减)
    • 设置传送方向:cld,std

cld,std

  • cld:FLAGS寄存器方向位(bit10)清零,此时传送方向,低地址向高地址传送
  • sld:FLAGS寄存器方向位(bit10)置一,此时传送方向,高地址向低地址传送

SECTION,ALIGN,VSTART

section给程序分段,align执行段的内存对齐方式,在同一个程序中,由多个段,理论每个段中的第一个数据地址是0,但是实际上是相对程序开头的地址,所以需要实现某个段的首个数据地址是0时,需要使用VSTART=0(0为指定起始地址,如果为2则每个段其实数据地址为2)

SECTION data1 ALIGN=16 VSTART=0
	mydata dw 0xFACE

SECTION data2 ALIGN=16 VSTART=0
	string db 'hello'

section code ALIGN=16 VSTART=0
	mov bx,mydata
	mov si,string

section.段名字.start

计算一个段相对整个程序的起始位置,相当于这个段位于程序开头的字节数,也可以是理解为相对程序开头的偏移量(示例见用户程序头部信息)

equ

等同于。常量的声明,类似于#define。常量的声明不会占用汇编地址

resb,resw,resd

在编译过程中保留指定大小的空间,不做初始化

resb 256 == resw 128 == resd 64 == times 256 db 0

in,out

端口的输入输出不能直接使用mov指令,在intel中需要使用in,out;它们都不影响标志位

  • in:源操作数通常是dx(存放端口号),目标操作数根据数据大小使用al或ax,当端口号小于256时,源操作数可以使用立即数
in al, dx
in ax, dx

; 例子:
mov dx, 0x3c0
in ax,dx

in al, imm8
in ax, imm8
; 例子
in al, 0x60
in ax, 0xa7
  • out:目标操作数通常为dx或小于256的立即数,源操作数为al或ax(和in相反)
out dx/imm8, al/ax

; 例子
out 0x60, al
out 0xa7, ax

mov dx, 0x3c0
out dx, ax

call指令

调用程序(16位相对近调用,所以跳转时cs不用压栈保存数据)。被调用的子程序以ret或retf指令结尾,过程返回,以此返回主程序继续执行。ip指针寄存器指向下一条指令的偏移地址,调用前ip压栈,返回时出栈。

  • call 标号或者目标处的汇编地址(16位相对近调用,前后两个字节的大小-32768到32767,可搭配ret
    • call dest
  • call 寄存器/偏移地址(段内,16位间接绝对近调用,可搭配ret
    • call cx:转移前先将ip压栈,然后用cx中的值填充ip,再进行跳转
    • call [0x3000]:转移前将ip压栈,然后ds值左移4位加上偏移地址0x3000取到物理地址,在地址中取出一个字(两个字节),填充到ip,再进行跳转
    • call [bx]:转移前将ip压栈,然后ds+bx得到物理地址,在地址中取出一个字(两个字节),填充到ip,再进行跳转
  • call 16位段地址:16位偏移地址(16位直接绝对远调用,可搭配reft
    • call 0x052e:0x003c:转移前将ip压栈,0x052e替换cs内容,0x003c替换ip内容
  • call far 目标地址(16位间接绝对远调用,可搭配reft)
    • call far [0x2000]:转移前将cs,ip压栈,然后ds值左移4位加上偏移地址0x2000取到物理地址,在地址中取出两个个字(四个字节),前一个字填充到ip,后一个字填充到cs,再进行跳转
    • call far [bx]:转移前将cs,ip压栈,然后ds+bx得到物理地址,在地址中取出两个个字(四个字节),前一个字填充到ip,后一个字填充到cs,再进行跳转

ret,retf

ret 用栈中的数据修改IP(近转移)
retf 用栈中数据修改cs:ip(远转移)

他们不一定要搭配call执行使用,也可搭配push使用,push先压缩段地址,在压缩偏移地址,然后调用retf,将从栈中pop出数据填充到cs和ip当中

条件跳转

  • js:符号标志SF为1则转移,jns,符号标志为0则转移

  • jz:零标志ZF为1则转移,jnz:零标志ZF为0则转移,

  • jo:溢出标志OF为1则转移,jno:溢出标志OF为0则转移

  • jc:进位标志CF为1则转移,jnc:进位标志CF为0则转移

  • jp:奇偶标志PF为1则转移,jnp:奇偶标志PF为0则转移

  • jcxz:当CX寄存器内容为0时则转移

  • cmp,比较两个数后,条件转移(大于等于小于等等)

    • 助记:
      • a:above高于
      • b:below低于
      • e:equal相等
      • n:not不
      • g:greater大于
      • l:小于
        • 例子:jnge:不大于等于
  • 无条件转移(jmp)

    jmp 之后的指令如果跟了 ret,那就直接退出到上一个 call 对应指令的下一行;jmp跳转后会影响cs,ip的值,但不会影响ds,es的值

    • jmp short 标号/目标处的汇编地址(段内,相对短转移,前后一个字节的大小-128到127)
    • jmp near 标号/目标处的汇编地址(段内,相对近转移,前后两个字节的大小-32768到32767)
    • jmp 标号/目标处的汇编地址(编译器决定时short还是near)
    • jmp 寄存器/内存地址(段内,16位间接绝对近转移)
    • jmp 16位段地址:16位偏移地址 (段之间转移,16位直接绝对远转移)
      • jmp 0x052e:0x003c:0x052e替换cs内容,0x003c替换ip内容
    • jmp far 内存地址(16位间接绝对远转移)
      • jmp far [0x2002]:20022003替换ip内容,20042005替换cs内容
      • jmp far [bx]:ds+bx拿到物理地址中数据中的两个字,第一个字替换ip内容,第二个字替换cs内容

指令对flags寄存器的影响

  • cbw / cwde / cdqe / cwd / cdq/ cqo: 不影响任何标志位。
  • cld : DF=0,对CF、OF、ZF、SF、AF和PF的影响未定义。
  • std : DF=1,不影响其他标志位。
  • inc/dec: CF标志不受影响;对OF、SF、ZF、AF和PF的影响依计算结果而定。
  • add / sub:OF、SF、ZF、AF、CF和PF的状态依计算结果而定。
  • div / idiv:对CF、OF、SF、ZF、AF和PF的影响未定义。
  • mov / movs:这类指令不影响任何标志位。
  • neg:如果操作数为0,则CF=0,否则CF=1;对OF、SF、ZF、AF和PF的影响,计算结果而定。
  • xor:OF=0,CF=O;对SF、ZF和PF依计算结果而定;对AF的影响未定义。

寻址方式

寄存器寻址

操作数时由寄存器引起的

  • mov ax, cx
  • add bx, 0xf000(目标操作数)
  • inc dx

立即数寻址

操作数时由立即数直接指定的

  • add bx, 0xf000(源操作数)
  • mov dx, mydata

内存寻址

数据在内存当中(8086:段地址左移4位加上偏移地址形成20位的物理地)

段地址:由4个段寄存器之一提供(cs, ds,es,ss,默认ds),偏移地址:由指令来提供(内存寻址,实际即使寻找偏移地址)

  • 直接内存寻址:
    • mov ax, [0x5c0f]
    • add word [0x0230], 0x5000(目标操作数)
    • xor byte [es:mydata], 0x55(目标操作数)
  • 变址寻址:内存寻址的一种
    • 类似基址寻址,不同之处主要在于变址寻址是使用si和di索引寄存器
; 使用变址寻址的例子

mov [si],dx
add ax,[di]
xor word [si], 0x8000 ; si提供偏移地址时,默认使用ds提供段地址

mov [si+0x100], al; 8086允许变址寄存器和基址寄存器加上偏移量使用
and byte [di+mydata], 0x80 ; and 位相与

基址寻址

在8086中由基址寄存器bx,bp提供,基址地址也称有效地址(effective address)

	buffer dw 0x20, 0x100, 0x0f,0x300, 0xff00(buffer是标号)
; 以下使用直接寻址方式将所有数据加一

	inc word [buffer]
	inc word [buffer+2]
	inc word [buffer+4]
	inc word [buffer+6]
	inc word [buffer+8]

; 以下使用基址寻址方式将所有数据加一
	mov bx, buffer
	mov cx, 5
lpinc:
	inc word, [bx]
	add bx, 2
	loop ipinc
	
; 越过出栈操作,把栈当作普通段访问
	mov ax, 0x5000
	mov bx, 0x7000
	mov cx, 0x8000
	push ax
	push bx
	push cx
	
	mov bx, sp ; bx指向栈底
	mov dx, [ss:bx+2] ; bx+2:8086允许基址寄存器加一个偏移量,不会影响bx和sp的值,只是形成一个和
	pop ax ; 出栈用哪个寄存器来接受没有限制
	pop bx
	pop cx ; 维持栈平衡(压多少,出多少)
	
; bp基址寄存器的使用
	mov ax, 0x5000
	mov bx, 0x7000
	mov cx, 0x8000
	push ax
	push bx
	push cx
	
	mov bp, sp
	mov dx, [bp+2] ; bp提供偏移,默认使用ss段寄存器提供段地址,所以可以省略段超越前缀ss:([ss:bp+2])
	pop ax
	pop bx
	pop cx

基址变址寻址:

基址变址寻址中使用了bx时,默认使用ds提供段地址,访问数据段;使用了bp时,默认使用ss提供段地址,访问栈段(使用示例见示例代码基址变址寻址的使用项中)

[bx + si]
[bx + di]
[bx + si + 偏移量]
[bx + di + 偏移量]

[bp + si]
[bp + di]
[bp + si + 偏移量]
[bp + di + 偏移量]

mov ax, [bx + si + 0x03]

用户程序

  • 用户程序头部必须包含:程序的长度,入口点,段重定位表项数,段重定位表
  • 用户程序起始逻辑扇区号从100开始存放(协定)

设备交互

硬盘(块设备)

  • 扇区时硬盘的基本单位,和cpu交互数据时是按一个扇区一个扇区的交互
  • 个人计算机上硬盘分配了8个端口0x1f0~0x1f7
7 6 5 4 3 2 1 0
固定值1 0:CHS模式;1:LBA模式 固定值1 0:主硬盘;1:从硬盘 逻辑扇区号位27 逻辑扇区号位26 逻辑扇区号位25 逻辑扇区号位24
7 6 5 4 3 2 1 0
BSY:为1则表示硬盘忙 DRQ:为1表明硬盘已准备好与主机交换数据 ERR:为1则表明前一个命令执行错误。具体原因可访问端口0x1f1
; LBA28读数据
;第一步:告诉硬盘要操作的扇区数
mov dx, 0x1f2
mov al, 0x01;需要操作一个扇区,如果这个数字是0,非常特殊,它代表要读写256个扇区
out dx, al

;第二步:设置起始LBA号
;扇区号=0000 00000000 00000000 00000010(28位)
mov dx, 0x1f3
mov al, 0x02
out dx, al		;LBA地址7~0
inc dx			;0x1f4
mov al, 0x00
out dx,al		;LBA地址15~8
inc dx			;0x1f5
out dx,al		;LBA地址23~16
inc dx			;0x1f6
mov al,0xe0		;LBA模式,主硬盘,LBA27~24
out dx,al

;第三步:告诉硬盘读命令
mov dx,0x1f7	;0x1f7命令端口和断开端口
mov al, 0x20;读命令
out dx,al

;第四步:等待读写操作完成
mov dx,0x1f7
.waits:
	in al,dx
	and al,0x88
	cmp al,0x08
	jnz .waits	;不忙,且硬盘已经准备好数据传输
	
;第五步:连续读出数据
;假定DS已指向存放扇区数据的段,BX里是段内偏移地址
mov cx,256		;总共要读取的字数
mov dx,0x1f0
.readw:
	in ax,dx
	mov [bx],ax
	add bx,2
	loop .readw

显卡

显卡提供两个端口0x3d4(索引端口),0x3d5(数据端口),显卡内部寄存器都有自己的编号,其中两个8位光标寄存器为0x0e(高8位),0x0f(低8位).

当我们把0x0f给索引端口时,数据端口会连接0x0f寄存器,我们就可以通过数据端口去读写数据。

put_char: ;显示一个字符
    ;输入:cl=字符ascii
    push ax
    push bx
    push cx
    push dx
    push ds
    push es

    ;以下取当前光标位置
    mov dx,0x3d4
    mov al,0x0e
    out dx,al
    mov dx,0x3d5
    in al,dx ;高8位
    mov ah,al

    mov dx,0x3d4
    mov al,0x0f
    out dx,al
    mov dx,0x3d5
    in al,dx ;低8位
    mov bx,ax ;BX=代表光标位置的16位数

    cmp cl,0x0d ;回车符?
    jnz .put_0a ;不是。看看是不是换行等字符
    mov ax,bx ;此句略显多余,但去掉后还得改书,麻烦
    mov bl,80
    div bl
    mul bl
    mov bx,ax
    jmp .set_cursor

.put_0a:
    cmp cl,0x0a ;换行符?
    jnz .put_other ;不是,那就正常显示字符
    add bx,80
    jmp .roll_screen

    .put_other: ;正常显示字符
    mov ax,0xb800
    mov es,ax
    shl bx,1
    mov [es:bx],cl

    ;以下将光标位置推进一个字符
    shr bx,1
    add bx,1

.roll_screen:
    cmp bx,2000 ;光标超出屏幕?滚屏
    jl .set_cursor

	push bx
    mov ax,0xb800
    mov ds,ax
    mov es,ax
    cld
    mov si,0xa0
    mov di,0x00
    mov cx,1920
    rep movsw
    mov bx,3840 ;清除屏幕最底一行
    mov cx,80
.cls:
    mov word[es:bx],0x0720
    add bx,2
    loop .cls

    pop bx
    sub bx,80

.set_cursor:
    mov dx,0x3d4
    mov al,0x0e
    out dx,al
    mov dx,0x3d5
    mov al,bh
    out dx,al
    mov dx,0x3d4
    mov al,0x0f
    out dx,al
    mov dx,0x3d5
    mov al,bl
    out dx,al

    pop es
    pop ds
    pop dx
    pop cx
    pop bx
    pop ax

    ret

中断

NMI:不可屏蔽中断

INTR:可屏蔽中断

示例代码

1+2+3....+100

jmp start

message:
    db '1+2+3+...+100=' ;等同于db '1','+','2','+',.....'='

start:
    mov ax, 0x7c0
    mov ds, ax

    mov ax, 0xb800
    mov es, ax

    mov si, message
    xor di, di
    mov cx, start - message

showmsg:
    mov al, [si]
    mov [es:di], al
    inc di
    mov byte [es:di], 0x07
    inc di
    inc si
    loop showmsg

    xor ax, ax
    mov cx, 1

summate:
    add ax, cx
    inc cx
    cmp cx, 100
    jle summate

    xor cx, cx
    mov ss, cx
    mov sp, cx

    mov bx, 10
decompo:
    inc cx
    xor dx, dx
    div bx
    add dl, 0x30
    push dx
    cmp ax, 0
    jne decompo

shownum:
    pop dx
    mov [es:di], dl
    inc di
    mov byte [es:di], 0x07
    inc di
    loop shownum

    jmp $

    times 510-($-$$) db 0
    db 0x55, 0xaa

基址变址寻址的使用

; 使用基址变址寻址就地反转字母,而不适用栈
jmp start
string db 'abcdefghijklmnopqrstuvwxyz'
start:
	mov ax, 0x7c0
	mov ds, ax
	
	mov bx,string			;数据区首地址
	mov si,0				;正向索引
	mov di,start-string-1	;方向索引
	
rever:
	mov ah,[bx+si]
	mov al,[bx+di]
	mov [bx+di],al
	mov [bx+si],ah			;首尾交换
	inc si
	dec di
	cmp si,di
	jl rever

用户程序头部信息

; 包含代码段、数据段和栈段的用户程序

;=======================================================================
section header vstart=0                         ;用户程序头部段
    ;程序总长度[0x00]
program_length:
    dd  program_end
    
    ;用户程序入口点
code_entry:
    dw  start                                   ;偏移地址[0x40]
    dd  section.code.start                      ;段地址[0x06]

    ;段重定位表项个数[0x0A]
realloc_tbl_len:
    dw  (segtbl_end-segtbl_begin)/4

    ;段重定位表
segtbl_begin:
    code_segment    dd  section.code.start      ;[0x0c]
    data_segment    dd  section.data.start      ;[0x19]
    stack_segment   dd  section.stack.start     ;[0x14]
segtbl_end:


;=======================================================================
section code align=16 vstart=0                  ;代码段
start:
    ;初始化执行时,DS和ES指向用户程序头部段
    mov ax,[stack_segment]                      ;设置到用户程序自己的堆栈
    mov ss,ax
    mov sp,stack_pointer                        ;设置初始的栈顶指针

    mov ax,[data_segment]                       ;设置到用户程序自己的数据段
    mov ds,ax

    mov ax,0xb800
    mov es,ax
    
    mov si,message
    mov di,0

next:
    mov al,[si]
    cmp al,0
    je exit

    mov byte [es:di],al
    mov byte [es:di+1],0x07
    inc si
    add di,2
    jmp next

exit:
    jmp $


;=======================================================================
section data align=16 vstart=0                  ;数据段
message:
    db 'hello world.',0


;=======================================================================
section stack align=16 vstart=0
    resb 256
stack_pointer:


;=======================================================================
section trail align=16
program_end:

硬盘主引导扇区代码(加载程序)

;文件说明:硬盘主引导扇区代码(加载程序)

app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号)
;常数的声明不会占用汇编地址

SECTION mbr align=16 vstart=0x7c00
    ;设置堆栈段和栈指针
    mov ax,0
    mov ss,ax
    mov sp,ax

    mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址
    mov dx,[cs:phy_base+0x02]
    mov bx,16
    div bx
    mov ds,ax ;令DS和ES指向该段以进行操作
    mov es,ax

    ;以下读取程序的起始部分
    xor di,di
    mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号
    xor bx,bx ;加载到DS:0x0000处
    call read_hard_disk_0

    ;以下判断整个程序有多大
    mov dx,[2]
    mov ax,[0]
    mov bx,512 ;512字节每扇区
    div bx
    cmp dx,0
    jnz @1 ;未除尽,因此结果比实际扇区数少1
    dec ax ;已经读了一个扇区,扇区总数减1
@1:
    cmp ax,0 ;考虑实际长度小于等于512个字节的情况
    jz direct

    ;读取剩余的扇区
    push ds ;以下要用到并改变DS寄存器

    mov cx,ax ;循环次数(剩余扇区数)

@2:
    mov ax,ds
    add ax,0x20 ;得到下一个以512字节为边界的段地址
    mov ds,ax

    xor bx,bx ;每次读时,偏移地址始终为0x0000
    inc si ;下一个逻辑扇区
    call read_hard_disk_0
    loop @2 ;循环读,直到读完整个功能程序
    pop ds ;恢复数据段基址到用户程序头部段


;计算入口点代码段基址
direct:
	mov dx,[0x08]
    mov ax,[0x06]
    call calc_segment_base
    mov [0x06],ax ;回填修正后的入口点代码段基址

    ;开始处理段重定位表
    mov cx,[0x0a] ;需要重定位的项目数量
    mov bx,0x0c ;重定位表首地址

realloc:
    mov dx,[bx+0x02] ;32位地址的高16位
    mov ax,[bx]
    call calc_segment_base
    mov [bx],ax ;回填段的基址
    add bx,4 ;下一个重定位项(每项占4个字节)
    loop realloc

    jmp far [0x04] ;转移到用户程序

;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
    ;输入:DI:SI=起始逻辑扇区号
    ; DS:BX=目标缓冲区地址
    push ax ;子程序,把本子程序的寄存器数据压栈,子程序结束前恢复他们
    push bx
    push cx
    push dx

    mov dx,0x1f2
    mov al,1
    out dx,al ;读取的扇区数

    inc dx ;0x1f3
    mov ax,si
    out dx,al ;LBA地址7~0

    inc dx ;0x1f4
    mov al,ah
    out dx,al ;LBA地址15~8

    inc dx ;0x1f5
    mov ax,di
    out dx,al ;LBA地址23~16

    inc dx ;0x1f6
    mov al,0xe0 ;LBA28模式,主盘
    or al,ah ;LBA地址27~24
    out dx,al

    inc dx ;0x1f7
    mov al,0x20 ;读命令
    out dx,al

.waits:
    in al,dx
    and al,0x88
    cmp al,0x08
    jnz .waits ;不忙,且硬盘已准备好数据传输

    mov cx,256 ;总共要读取的字数
    mov dx,0x1f0
.readw:
    in ax,dx
    mov [bx],ax
    add bx,2
    loop .readw

    pop dx
    pop cx
    pop bx
    pop ax

    ret

;-------------------------------------------------------------------------------
calc_segment_base: ;计算16位段地址
    ;输入:DX:AX=32位物理地址
    ;返回:AX=16位段基地址
    push dx

    add ax,[cs:phy_base]
    adc dx,[cs:phy_base+0x02]
    shr ax,4
    ror dx,4
    and dx,0xf000
    or ax,dx

    pop dx

    ret

;-------------------------------------------------------------------------------
phy_base dd 0x10000 ;用户程序被加载的物理起始地址

    times 510-($-$$) db 0
    db 0x55,0xaa