用户程序的结构
处理器的工作模式是将内存分成逻辑上的段,指令的获取和数据的访问一律按“段地址:偏移地址”的方式进行
一个规范的程序,应当包括代码段、数据段、附加段、栈段
段的划分和段与段之间的界限在程序加载到内存之前就已经准备好了
以下是一个用户程序代码组织结构

分析:
-
分段:
SECTION 段名称或SEGMENT 段名称,上图就定义了6个段,一旦定义段,后面的内容都属于该段,除非又出现了另一个段的定义 -
段的汇编地址:
align=16(32)表示段在内存中的起始物理地址是按16(32)字节对齐的,简单来说就是 该物理地址能够被16(32)整除,所以段的长度也是16(32)的倍数 -
段内汇编地址:尽管定义了段,但标号的汇编地址还是从整个程序的开头计算的,而不是从段的开头计算的,这可以使用
vstart=0子句。vstart指明了段内汇编地址的起始位置在上图中,code段内有一个标号putch,因为code段定义的时候使用了vstart=0语句,这表示putch标号的汇编地址是相对于code这个段的并从0开始计算,而不是整个程序的。(如果vstart=1,那它的汇编地址就相对于code这个段并从1开始计算)
trail段内有一个标号program_end,该段定义的时候没有使用vstart语句,所以program_end标号的汇编地址是相当于整个程序的,这里我们可以将它的汇编地址视为整个程序的长度
用户程序头部:

在刚才的分析中,我们能够看到定义的第一个段是叫做header,这个很重要。header段内一般保存程序的一些信息,方便加载器使用,这是用户程序与加载器之间的协议(约定)。所以在定义段的时候要注意遵守
用户程序头部包含的基本信息:
- 用户程序的尺寸(字节为单位): 加载器需要根据这个信息来决定读取多少个逻辑扇区
- 应用程序的入口点(段地址和偏移地址):指定用户程序运行时的第一条指令。先写入偏移地址(2字节),在写入段地址(4字节),偏移地址一般用标号给出,段地址则是语句
section.段名.start给出- 在实模式下(16位),段最长为64KB,所以段的偏移地址可以用16位表示,但段的起始地址可以是任何20位的物理地址处,所以段的起始地址要用32位表示
- 疑惑:Intel处理器要求段地址起码是16字节对齐的,也就是二进制形式低4位为0,16进制最低位为0,那有效的不就是高4位(16进制),那这也就可以用16位表示了啊,有点没太理解为啥用32位
- 段重定位表:用户程序头部中使用的地址都是程序编译阶段的汇编地址,并不是实际的地址,当加载器将用户程序加载到内存中,会根据加载的实际位置进行重新计算,这个就是重定位。段的重定位是加载器的工作,它需要知道每个段在用户程序内的位置,也就是汇编地址,所以需要建立一张段重定位表,每个表项占用4个字节
以下是用户程序头部的示例:
SECTION header vstart=0 ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start ;偏移地址[0x04]
dd section.code_1.start ;段地址[0x06]
realloc_tbl_len dw (header_end-code_1_segment)/4
;段重定位表项个数[0x0a]
;段重定位表
code_1_segment dd section.code_1.start ;[0x0c]
code_2_segment dd section.code_2.start ;[0x10]
data_1_segment dd section.data_1.start ;[0x14]
data_2_segment dd section.data_2.start ;[0x18]
stack_segment dd section.stack.start ;[0x1c]
header_end:
用户程序的工作流程
可以先向后看,了解了加载器的工作流程再来看这一节,写在这里是因为上一节介绍了用户程序的结构
-
初始化段寄存器和栈切换
当加载器完成了重定位工作后,用户程序可以在处理器上执行了
最先要做的就是 初始化各个段寄存器DS、ES、SS (CS不用初始化,由加载器负责) 以便访问专属于自己的数据
-
初始化数据段
、
刚进入用户程序时,ds和es都指向段header(加载器中是这么处理的),现在我们将ds指向我们自己的数据段mov ax,[data_1_segment] ;设置到用户程序自己的数据段 mov ds,ax -
初始化栈段
首先,我们要预留一部分内存作为栈空间,伪指令resb意为reserve byte,意思是 从当前位置开始,保留指定数量的字节,但不初始化值,编译阶段会保留这段内存区域,即跳过指定数量的字节
还有其他类似的指令,比如resw(字), resd(双字)
SECTION stack align=16 vstart=0 resb 256 stack_end:然后,初始化寄存器
mov ax,[stack_segment] ;设置到用户程序自己的堆栈 mov ss,ax mov sp,stack_end
-
-
填充代码段和数据段,也就是写具体要做什么(例程),这里就不详细介绍了
补充
有时候,用户程序中有多个数据段和代码段,如何在不同的段中切换也是相当重要的
-
切换到另一个代码段
其实很简单,就是将目标段的CS(段地址)和IP(入口点偏移地址)值压入栈中,然后通过ret或retf指令实现段间转移,目标段的信息都在段header中哦,我们还有个es寄存器指向段header
SECTION code_1 align=16 vstart=0 ;定义代码段1(16字节对齐) ..... ..... ;省略前面的代码,这里只展示代码段的转移 push word [es:code_2_segment] mov ax,begin push ax ;可以直接push begin,80386+ retf ;转移到代码段2执行 continue: ....... SECTION code_2 align=16 vstart=0 ;定义代码段2(16字节对齐) begin: push word [es:code_1_segment] mov ax,continue push ax ;可以直接push continue,80386+ retf ;转移到代码段1接着执行 -
访问了另一个数据段
这个也很简单,只需将ds寄存器指向另一个数据段即可,目标段的信息都在段header中哦,我们还有个es寄存器指向段header
mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2 mov ds,ax
访问硬件
外围设备及其接口
- 外围设备:和计算机主机连接的设备,一般分为输入设备和输出设备(输入输出设备)
- I/O接口:I/O接口像是一个翻译器一样,实现外围设备和处理器之间的信号通信。不同的外围设备,都有各自不同的I/O接口
- 总线技术:总线可以认为是一排公共电线,所有的外围设备和处理器都连接到总线上---->避免了处理器连接所有I/O接口
- 输入输出控制设备集中器(ICH)芯片:连接不同的总线,并协调各个I/O接口对处理器的访问---->避免了多个I/O接口争着访问处理器的冲突

为了支持更多的设备,ICH提供了对PCI Express总线的支持,该总线向外延伸,连接着主板上的若干个扩展槽
除了局部总线和PCI Express总线,每个I/O接口卡可能连接不止一个设备
当处理器相同某个设备对话是,ICH会接到通知。然后,它负责提供相应的传输通道和其他辅助支持并命令所有其他无关的设备闭嘴。当某个设备相同处理器对话时也一样
I/O端口和端口访问
-
端口:
- 端口是处理器和外围设备通过I/O接口交流的窗口,每一个I/O接口都可能拥有好几个端口,分别用于不同的目的
- 端口本质上就是一些寄存器,位于I/O接口电路中
- 例如硬盘的PATA/SATA接口的端口:
- 命令端口:向该端口写入0x20表示从硬盘读数据,写入0x30表示向硬盘写数据
- 状态端口:处理器根据该端口的数据来判断硬盘工作是否正常
- 参数端口:处理器通过这些端口高数硬盘读写的扇区数,起始逻辑扇区号
- 数据端口:通过该端口连续取得读出的数据或发送写入的数据
-
端口的实现方式:
-
内存映射:端口号是映射到地址空间的,访问这部分内存实际上是在访问I/O接口
-
独立编址:不和内存发生关系,处理器的地址线既连接内存,也连接每一个I/O接口。引脚M/IO#用来打开相关电路,#表示低电平有效,处理器访问内存时,它会让M/IO#引脚呈高电平,打开和内存相关的电路;访问接口时,则是低电平
![dilibianzhi]()
所有端口都是统一编号的,比如个人计算机中的PATA/SATA接口,每个PATA和SATA接口分配了8个端口,ICH芯片内部通常集成了两个PATA/SATA接口,分别是主硬盘接口和副硬盘接口。
主硬盘接口分配的端口号是 0x1f0-0x1f7
副硬盘接口分配的端口号是 0x170-0x177 -
-
访问端口:
- in指令:从端口读
- 一字节形式:
in al/ax, dx,操作数只能是这几个寄存器,al/ax对应端口的宽度使用,所以机器码简单,只有1字节 - 双字节形式:
in al/ax, 立即数(端口号),操作数只允许一字节,所以只能访问0-255号端口
- 一字节形式:
- out指令:向端口写
out 立即数(端口号)/dx, al/ax,out指令的操作数更灵活
- in指令:从端口读
通过硬盘控制器端口读扇区数据
硬盘读写的基本单位是扇区,所以主机和硬盘之间的数据交换是成块的,硬盘是典型的块设备
-
从硬盘读写数据的方式:
- CHS模式:物理位置,向硬盘控制器分别发送磁头号、柱面号、扇区号
- LBA模式:逻辑扇区,所有的扇区统一编址,从0开始编号(有个从CHS到LBA的公式,不过我忘了)
- LBA28编址:用28个比特来表示逻辑扇区号,共2^28个扇区,每个扇区512字节,可以管理 2^28x512B=128GB 的硬盘
- LBA48编址:与上面差不多,可以管理 131072TB 的硬盘
-
从硬盘上读逻辑扇区:
-
设置要读取的扇区数量:
- 数值写入 0x1f2 端口,8位端口,每次只能读写255扇区
- 写入值为0,表示读取256个扇区
- 每读一个扇区,数值减1,读写报错,则该端口包含着未读取的扇区数
mov dx, 01f2 ; dx存储端口号 mov al, 0x01 ; 1个扇区 out dx, al ; 将扇区数写入端口 -
设置起始LBA扇区号:
-
扇区的读写是连续的(逻辑上连续),只需起始扇区号就行
-
28位的扇区号分成 4段
- 0-7位写入端口 0x1f3
- 8-15位写入端口 0x1f4
- 16-23位写入端口 0x1f5
- 最后4位写入端口 0x1f6,以下是端口0x1f6各个位的含义
![1f6]()
-
mov dx, 0x1f3 mov al, 0x02 ; 起始逻辑扇区号为2 out dx, al ; LBA地址0-7位写入0x02 inc dx ; 0x1f4 mov al, 0x00 out dx, al ; LBA地址8-15位写入0x00 inc dx ; 0x1f5 out dx, al ; LBA地址16-23位写入0x00 inc dx ; 0x1f6 mov al, 0xe0 ; LBA模式,主硬盘,以及LBA地址24-27位 out dx, al ; LBA地址24-27位写入0xe0,准确来说低4位都是0,高4位需要具体数值来控制访问的模式- 向端口 0x1f7写入 0x20,请求硬盘读,8位端口
mov dx, 01f7 mov al, 0x20 ; 读命令 out dx, al- 等待读写操作完成:端口 0x1f7 既是命令端口,又是状态端口
![1f7]()
以上是该端口的部分状态位,第7位为1表示硬盘忙,当硬盘系统准备就绪后,该位清零。第3位为1表示准备好了,请求主机发送或接受数据mov dx, 0x1f7 .waits: in al, dx and al, 0x88 ; 0x88的二进制为10001000,这里是保留第7位和第3位,其余清零 cmp al, 0x08 ; 0x08的二进制为00001000, 如果相等,说明硬盘准备好了,退出等待 jnz .waits- 连续取出数据:端口 0x1f0 是硬盘接口的数据端口,16位端口,硬盘控制器空闲且准备就绪,就可以连续从该端口读取或写入数据
; 假定将读取数据存放到ds:bx处 mov cx, 256 ; 总共要读取的字数,一个字等于两个字节 mov dx, 0x1f0 .readw: in ax, dx mov [bx], ax add bx, 2 loop .readw -
过程调用
又可称为例程、子程序、子过程、子例程,其实就是一串普通代码,就像高级语言中的函数一样,方便复用
处理器可以用过程调用指令转移到这段代码执行,在遇到过程返回指令时重新返回到调用处的下一条指令接着执行

保护现场和恢复现场其实就是先将会被过程(函数)修改的寄存器的值压入栈中保护起来,等过程结束要返回调用处之前,将寄存器的值恢复,也就是出栈
过程调用指令 call :
- 近调用:被调用的目标过程位于当前代码段内,只需得到偏移地址即可
- 16位相对近调用:
call near 标号或立即数或call 标号或立即数- 操作数是偏移量,16位有符号数
- 三字节指令
- 计算新的IP值,将原有的IP值压入栈,替换新的IP值
- 16位间接绝对近调用:
- 操作数是被调用过程的真实偏移地址(绝对地址),由16位寄存器或内存单元间接给出
- 16位相对近调用:
- 远调用:段间调用,即调用另一个代码段内的过程,需要段地址和偏移地址
- 16位绝对直接远调用:
call 段地址:偏移地址- 16位用于针对偏移地址,而不是限定段地址(虽然也是16位)
- 依次压入cs和ip的值,在用指令中的值替换寄存器的内容
- 16位间接绝对远调用:
call far [内存单元或寄存器],需注意段地址在高位,偏移地址在低位
- 16位绝对直接远调用:
过程返回指令:ret和retf,就是将call指令保存到栈中的原有的IP和CS恢复
- ret 和 call 配对, 仅需恢复IP
- retf 和 call far 配对,需要恢复IP和CS
加载程序(器)的工作流程
-
初始化和决定加载位置(内存---加载的物理地址,硬盘---起始逻辑扇区号)
- 看内存中的什么地址是空闲的,即从哪个物理内存地址开始加载用户程序
phy_base dd 0x10000,这个语句表示加载的物理地址设置为0x10000
![phy_base]()
- 用户程序位于硬盘上的什么位置,它的起始逻辑扇区号是多少
app_lba_start equ 100,这个语句表示起始逻辑扇区号是100,用equ声明的是常数,不占用任何汇编地址,仅仅代表一个数值
- 看内存中的什么地址是空闲的,即从哪个物理内存地址开始加载用户程序
-
准备加载用户程序
- 将主引导扇区定义成一个段:
SECTION mbr align=16 vstart=0x7c00 - 初始化堆栈
- 获取加载的地址,起始物理地址---->段地址(ax存低16位,dx存高16位,除以16相当于右移4位,转变成段地址,存放在ax中)
![获取加载地址]()
; 计算用于加载用户程序的逻辑地址 mov ax, [cs:phy_base] ; 获取加载的起始物理地址 mov dx, [cs:phy_base + 0x02] mov bx, 16 div bx ; 除以16表示右移4位,ax中存储的商就是段地址 mov ds, ax ; 令ds和es指向该段以进行操作 mov es, ax - 将主引导扇区定义成一个段:
-
从硬盘读取用户程序:见访问硬件小节,以下是一个读取硬盘数据的例程
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地址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 -
加载用户程序
注意,图上的地址是段内的汇编地址哦,并不是实际的物理地址
![用户程序头部结构]()
;以下读取程序的起始部分
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 ; 总长度除以512字节=总扇区数
cmp dx,0
jnz @1 ;未除尽,因此结果比实际扇区数少1
dec ax ;已经读了一个扇区,扇区总数减1
@1:
cmp ax,0 ;考虑实际长度小于等于512个字节的情况
jz direct ;这里ax=0,表示已经读完了,所以直接跳到标号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 ;恢复数据段基址到用户程序头部段
观察上面 读取剩余扇区的代码,我们看到它每次读取扇区都改变了ds的值,这说明,每次读取扇区的段地址都不同。这是因为一个逻辑段最大也才64KB,当用户程序过大,容纳不下时,偏移地址会回卷从而覆盖掉最开始加载的内容
所以这里将读取的数据加载到新段中,每个段都是512字节,所以相邻段地址相差0x20(0x200就是512),正因为每次都是加载到新段,所以偏移地址每次都是从0开始,也就有了xor bx, bx
至此,我们就将用户程序加载到内存的指定位置了,就下来就是解决如何让处理器能够执行到用户程序的问题了
-
用户程序重定位
用户程序在编写的时候是分段的,但段地址只是编译阶段生成的汇编地址,并不是真正的物理地址,处理器无法访问到他们。所以处理器接下来的工作就是计算和确定每个段的地址事实上,这部分的工作并不困难,只要将每个段的汇编地址加上用户程序加载到内存中的起始物理地址就能得到段的物理地址了,简单来说就是每个段在内存中的物理地址是基于我们设置的加载物理地址(前面设置的phy_base)的
如下图所示

下面这段代码要结合加载用户程序这个章节的用户程序头部结构示意图来看,这样才知道每个内存单元代表什么
主要是两部分需要重定位,入口点代码段以及段重定位表
;计算入口点代码段基址
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
下面来解析下calc_segment_base这个重定位例程
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位物理地址
;返回:AX=16位段基地址
push dx
;计算重定位的物理段地址,32位
add ax,[cs:phy_base] ;汇编地址+加载基地址=重定位地址,可能产生进位
adc dx,[cs:phy_base+0x02] ;adc是带进位加法,操作数相加在加上CF位的值
;将计算出来的地址右移4位即可得到逻辑段地址,但是在两个寄存器中--->分别移动再拼接
;有效位只有ax的高12位,dx的低4位,合计16位段地址
shr ax,4
ror dx,4 ;循环右移4位,低4位成为高4位
and dx,0xf000 ;除高4位其他全部置0
or ax,dx ;拼接
pop dx
ret
逻辑右移(逻辑左移同): shl和shr指令

循环右移(循环左移同):rol和ror指令

- 将控制权交给用户程序
其实就是移动到用户程序入口点执行指令,入口点是连续的两个字,低位是偏移地址,赋给IP寄存器,高位是段地址,赋给CS寄存器
jmp far [0x04],由一个16位的简介绝对远转移指令实现
8086处理器的无条件转移指令
- 相对短转移:
jmp short 标号或立即数- 必须使用 short 关键字
- 操作数是偏移量,是个有符号数,仅1字节,所以范围在-128~127
- 近转移:段内转移
- 16位相对近转移:
jmp near 标号或立即数, near关键字可省略- 操作数是偏移量,2字节(16位)
- 16位绝对近转移:
jmp near 寄存器或内存地址, near关键字可省略
- 16位相对近转移:
- 远转移:段间转移
- 16位直接绝对远转移:
jmp 段地址:偏移地址- 16位仅仅用来限定偏移地址
- 16位间接绝对远转移:
jmp far 内存地址- 操作数(内存地址)可以是任何内存寻址方式
- 16位仅仅用来限定偏移地址
- far关键字让处理器从给定的偏移地址处取出两个字给CS和IP
- 16位直接绝对远转移:
MBR完整源代码
;代码清单8-1
;文件名:c08_mbr.asm
;文件说明:硬盘主引导扇区代码(加载程序)
;创建日期:2011-5-5 18:17
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] ;曾经把dx写成了ds,花了二十分钟排错
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






posted on
浙公网安备 33010602011771号