认识ARM64汇编

之前说过学习汇编就是学习寄存器和指令,查看代码请连接真机。

寄存器

arm64汇编中寄存器是64bit的,使用X[n]表示,低32位以w[n]表示

64位架构中有3164位的通用寄存器。

可以通过register read查看

(lldb) register read
General Purpose Registers:
        x0 = 0x0000000000000001
        x1 = 0x00000002805d8a20
        x2 = 0x00000002837e7980
        x3 = 0x00000002805d8a20
        x4 = 0x0000000000000001
        x5 = 0x0000000000000001
        x6 = 0x0000000100d54000  
        x7 = 0x0000000000000000
        x8 = 0x0000200000000000
        x9 = 0x000161a1d63f7945 (0x00000001d63f7945) (void *)0x01d63f7cb0000001
       x10 = 0x0000000000000000
       x11 = 0x000000000000006d
       x12 = 0x000000000000006d
       x13 = 0x0000000000000000
       x14 = 0x0000000000000000
       x15 = 0x00000001ca634e6d  "touchesBegan:withEvent:"
       x16 = 0x000000019c4cd47c  libobjc.A.dylib`objc_storeStrong
       x17 = 0x0000000000000000
       x18 = 0x0000000000000000
       x19 = 0x0000000100f17810
       x20 = 0x00000002837e7920
       x21 = 0x00000002805d8a20
       x22 = 0x00000001ca634e6d  "touchesBegan:withEvent:"
       x23 = 0x0000000100e11a30
       x24 = 0x00000002837e7980
       x25 = 0x0000000100e11a30
       x26 = 0x00000002805d8a20
       x27 = 0x00000001ca62d900  "countByEnumeratingWithState:objects:count:"
       x28 = 0x00000002805d8a20
        fp = 0x000000016f47d730
        lr = 0x00000001009866dc  ArmAssembly`-[ViewController touchesBegan:withEvent:] + 84 at ViewController.m:38
        sp = 0x000000016f47d720
        pc = 0x0000000100986720  ArmAssembly`foo1 + 16 at ViewController.m:46
      cpsr = 0x80000000
(lldb) 

继续阅读认识ARM64汇编

.align 5之类的知识

经常会看到 arm-linux 汇编中有如下的指令:

.align n 它的含义就是使得下面的代码按一定规则对齐。

.align n 指令的对齐值有两种方案:n 或 2^n 。各种平台最初的汇编器一般都不是gas ,采取方案12 的都很多,gas 的目标是取代原来的汇编器,必然要保持和原来汇编器的兼容,因此在gas 中如何解释.align 指令会显得有些混乱,原因在于保持兼容。arm-linux 是按照 2^n 的方案对齐的,需要说明的是这个对齐和ld-script 里的对齐不同,不是一会事。下面的英文就不同平台的对齐进行了说明:版本2.11.92.0.12gasinfo(Mandrake 8.2 上的) 这样说:

The way the required alignment is specified varies from system to system. For the a29k, hppa, m68k, m88k, w65, sparc, and Hitachi SH, and i386 using ELF format, the first expression is the alignment request in bytes. For example `.align 8` advances the location counter until it is a multiple of 8. If the location counter is already a multiple of 8, no change is needed.

For other systems, including the i386 using a.out format, and the arm and strongarm, it is the number of low-order zero bits the location counter must have after advancement. For example `.align 3' advances the location counter until it a multiple of 8. If the location counter is already a multiple of 8, no change is needed.

从这段文字来看,ARM.align 5就是 25次方对齐,也就是 4 字节对齐,通过反汇编也可以看出对齐方式:

.align 5

stmfd sp!, {r0 - r3, lr}

mov r0, ip

ldmfd sp!, {r0 - r3, pc}^

.align 5

stmfd sp!, {r0 - r3, lr}

mov r0, ip

mov ip, r0

ldmfd sp!, {r0 - r3, pc}^

反汇编:

00000000 <.text>:

0: e92d400f stmdb sp!, {r0, r1, r2, r3, lr}

4: e1a0000c mov r0, ip

8: e8fd800f ldmia sp!, {r0, r1, r2, r3, pc}^

...

20: e92d400f stmdb sp!, {r0, r1, r2, r3, lr}

24: e1a0000c mov r0, ip

28: e1a0c000 mov ip, r0

2c : e8fd800f ldmia sp!, {r0, r1, r2, r3, pc}^

30: e1a00000 nop (mov r0,r0)

34: e1a00000 nop (mov r0,r0)

38: e1a00000 nop (mov r0,r0)

3c : e1a00000 nop (mov r0,r0)

一些忠告:

In the future, everytime when you build an elf file, you need meantime created your map file. And then you will avoid mistakes like this align.

Also, please also pick up some linker script knowlege part. For embedded system, we frequently play the linker script to tune an image, for example, align some special section and so on for protection or/and cache purpose. wish helpful.

参考链接


.align 5之类的知识

王爽 汇编语言 课程设计2

;本程序在VMWARE中调试通过,可以写入软盘,并正常执行
;可以使用Bochs 但是操作细节比较复杂
;程序界面不够友好
;程序的切换方式是 ESC 注意不要进入时间设置就退不出来了.
;在时间设置界面没有设置提示语句,进去就是黑屏幕,不过只要输入数字就会显示.注意输入 的
;数字的大小,世界上没有24:00:00以后的时间.我也懒得去判断输入了.
;输入后按回车或者超过6个有效 数字,程序会自动设置时间,并切换回主界面
;在Windows 下看不到时间的更改,原因是,Windows只在系统引导的时候读取CMOS时间,并把它的
;初值给8253,其余时间都由8253为操作系统提供时钟,因此尽管在Windows状态下可以更改CMOS
;中的时间,但是右下脚的时间是不会产生变化的,也就是说更改时间是看不到的.关机的时候,
;系统把8253中的时间经过计算后写回CMOS。这点在WINDOWS下调试程序应当注意.

;在Windows 2000以后的操作系统中,系统内核不允许直接端口操作,因此,在DEBUG中执行IN,OUT
;操作是无效的.因此端口的调试应在9X或DOS下进行,比如在虚拟机中的Windows 98中.
;中断嵌套问题。
;以一个8253作为中断处理芯片来算,则同时可能发生的最大中断数量为8级,因此堆栈必须至少足够
;8个以上中断的最大数目才不会发生可能的问题。
;286 以上系统使用两片8259A级联管理15级可屏蔽中断。从中断0到中断15。比较常见的分配方式:
;       IRQ0  系统定时器
;      IRQ1  键盘
;      IRQ2  可编程中断控制器8259A
;      IRQ3  COM2 ( 串口 )
;      IRQ4  COM1 ( 串口 )
;      IRQ6  软盘控制器
;      IRQ7  并行口LPT1
;      IRQ8  系统CMOS/实时钟
;      IRQ12  PS/2鼠标
;       IRQ13  数学协处理器
;      IRQ14  第一IDE控制器(硬盘)
;      IRQ15  第二IDE控制器  (CDROM ) 
;                        IRQ5    可用     (如声卡) 
;                        IRQ9    可用     (如网卡)
 ;                       IRQ10  可用     (如USB)
 ;                       IRQ11  可用     (如SCSI主适配器)
;按照16级中断来处理,那么,在极端的情况下如果16级同时发生,则事先准备的堆栈区域必须满足
;16级中断的要求。每级中断至少14个寄存器。即14*16/8*16=448B, 堆栈应大于此数.

;CPU工作在 8086模式下,BIOS在初始化完系统后,在1M内存的使用情况

;384KB上位内存----- ************************
;                             *                                        * 
;                             *                                        * 
;保留                       *                                        * 
;                             *                                        *
;                             *                                        *
;640KB常规内存----- ************************------9FFFFH 
;                             *                                        * BIOS扩展数据区 1KB
;                             ************************------9FC00H
;                             *                                        * 
;                             *                                        *  ___607KB
;                             *                                        * 
;                             *                                        * 
;                             ************************------07E00H
;                             *                                        * 系统引导扇区安放位置 512B
;                             ************************ ------07C00H
;                             *                                        * 
;                             *                                        * _______29KB
;                             *                                        * 
;                             ************************------00800H
;                             *                                        *  BIOS数据区 1KB
;                             ************************------003FFH
;                             *                                        *  BIOS中断向量表 1KB
;                             ************************------00000H
;
;只可以使用常规的640K中的没有被占用的空间。

Assume cs:code
Data Segment
Error03 db 'Disk Write-Protect Error','$' ;磁盘写保护错误
Error04 db 'Can Not Find The Right Sector','$'    ;磁道寻找错误,软盘损坏
Error20 db 'Floppy Drive Error ,Maybe It Can Not Work !','$'  ;软盘不存在或工作部正常
Error80 db 'No Answer From Disk Drive!','$' ;驱动器不响应
ErrorExit db 'Press Any Key To Try Again Or Press ESC To Quit','$'
Data  Ends

Code Segment
Start:

;安装程序
;写入以下的程序到第一个和后面的扇区
;不需要确定程序的确切固定长度,因为int 13h只能按照512字节的倍数传递,所以即使比512多
;一个字节,也要传递512个。
mov ax,Data
mov ds,ax

mov ax,cs
mov es,ax   ;要写的内存基址
lea bx,Real ;要写的内存偏移

mov ah,3   ;功能号,写
mov al,((ProgrameEnd-Start)/512+1);写扇区的数目,浪费一个扇区以获得编写的方便。
mov ch,0   ;磁道号
mov cl,1   ;扇区号
mov dl,0   ;驱动器号
mov dh,0   ;磁头号(面)
int 13h   

;磁盘读写容错处理
cmp ah,0
je WriteFinish

cmp ah,03h
je WriteError03

cmp ah,04h
je WriteError04

cmp ah,20h
je WriteError20

cmp ah,80h
je WriteError80

WriteFinish:
mov ah,4ch
int 21h

WriteError03:
mov si,Offset Error03
jmp WriteErrorManage

WriteError04:
mov si,Offset Error04
jmp WriteErrorManage

WriteError20:
mov si,offset Error20
jmp WriteErrorManage

WriteError80:
mov si,Offset Error80
jmp WriteErrorManage

WriteErrorManage:
mov dh,4
mov dl,4
call near ptr Show_Str
mov si,Offset ErrorExit
mov dh,6
mov dl,4
Call near ptr Show_Str
mov ah,0
int 16h
Call near ptr ClearScrean
cmp al,1bh
jne Start
jmp WriteFinish

;实际程序体
Real:
;第一个扇区的数据
;第一个扇区只作为引导第二个扇区的引导程序 作用是把第二个扇区读到0:800处,
;并置cs:ip到0:800,原因是在引导现有的操作系统的时候,需要用到0:7c00内存区
;其中int 9h 需要占用0040:17作为状态字
;必须把0:7c00内存空闲出来。
;由于硬盘引导记录只能被存放到0:7c00处才能被正确引导,因此,需要在引导前把本程序
;移出内存。
;程序转移

;设置系统用堆栈内存区,由于没有操作系统支持,因此必须自己设定堆栈范围
;栈顶设置在9FBFF:0处,9FC00H(BIOS)保留数据区前一个位置.

cli ;避免堆栈设置时发生中断
mov ax,9000H             
mov ss,ax
mov ax,0FBFFH
mov sp,ax
sti

;内存基址设置,一次设置,以后不用更改,因为程序小于64K
mov ax,0
mov ds,ax
mov es,ax  

;读取第二和以后的扇区到内存的0:800处
mov bx,800h ;要读的内存偏移
mov ah,2   ;功能号,读
mov al,((ProgrameEnd-T)/512+1)   ;读扇区的数目,浪费512个字节以获得编写的方便。
mov ch,0   ;磁道号
mov cl,2   ;扇区号
mov dl,0   ;驱动器号
mov dh,0   ;磁头号(面)
int 13h   

;置CS,IP
mov ax,0
mov bx,800h
push ax
push bx
retf ;retf 相当于
     ;pop ip 
     ;pop cs 
     ;cs ip 的值不能通过普通的mov指令修改

;凑足一个扇区,主要满足int 19 对'55AA'标志的要求
;当前指令减去标号指令地址来计算应当的填充数据.
 db 510-($- Real) dup( 'A')

DW  0AA55H

;一扇区以后的扇区的数据
T:
jmp ReturnPoint;实际程序代码区
;Data Segment
Reset        db '1)Reset PC' ,0ah,0ah,0ah ;换行               ;'$'
StartS       db '2)Start System',0ah,0ah,0ah             ;'$'
Clock        db '3)Clock',0ah,0ah ,0ah                ;'$'
Set          db '4)Set Clock','$'
;时钟设置界面提示信息
SetTimeMessage db 'Please Input The Right Time :','$'

TimeBuffer db 3 dup(0),'e'    
                ;'$'的ASCII为36 即 0x24 会出现错误BCD 码中有0x24 e的ASCII为101 即 0x65   
               ;判断是否为终止符号
Counter dw 0 ;用在时钟设置中的字符输入计数器
TimeShowBuffer db 6 dup('0'),'$'
;Data ends

ReturnPoint: ;程序返回点
;设置屏幕显示
;清屏
call ClearScrean
;由于使用静态地址分配,因此只有采用标号差来计算距离数据区的长度,最终确定数据区的位置。
mov si,(Reset-T+800H)
mov dh,4
mov dl,12
call Show_Str

;循环读取键盘缓冲区
ReadKeybordBuffer:
mov ax,0 ;实际参数传递为ah ,但要在循环读取中要清空 al 因此直接清空 ax
int 16h
cmp al,'1'
je RebootFar ;程序超过JE的最大跳转范围
cmp al,'2'
je  StartSystemFar
cmp al,'3'
je TimeShow
cmp al,'4'
je Clock_SetFar
jmp ReadKeybordBuffer

Clock_SetFar:
jmp Clock_Set
RebootFar:
jmp Reboot
StartSystemFar:
jmp StartSystem

;时间显示
TimeShow:
call ClearScrean

;读取时间
TimeShowStart:
mov bx,(TimeBuffer-T+800H)
call ReadTime

;转换时间
mov bx,(TimeBuffer-T+800H)
mov si,(TimeShowBuffer-T+800H);偏移地址
call BCDChange

;向显示时间的格式转换
mov bx,(TimeShowBuffer-T+800H)
mov si,bx
call ChangeForShow
;屏幕显示
mov si,(TimeShowBuffer-T+800H)
mov dh,16
mov dl,20
call Show_Str

;检查键盘输入
mov ah,01h;检查缓冲区,若有字符则ZF=0,否则ZF=1 同时 AH=扫描码,AL=ASCII
int 16h ;在调用int 16h的过程中共需要多次PUSH数据,因此堆栈应满足需要(消耗100字节以内的内存)。 
jnz CheckCharacter
jmp TimeShowStart
CheckCharacter:
mov ah,00h
int 16h
cmp ah,01h ;判断是否为ESC
je ExitTimeShow
jmp TimeShowStart
ExitTimeShow:
call ClearScrean ;清屏

jmp ReturnPoint

Clock_Set:
;清屏
Call ClearScrean

mov si,(SetTimeMessage-T+800H)
mov dh,3
mov dl,6
Call Show_Str

;获取TimeShowBuffer的偏移地址作为字符串输入的接收内存
mov si,(TimeShowBuffer-T+800H)

;清除切换界面后内存的数据残留问题
mov ax,0
mov [si-2],ax
mov al,'0'
mov bx,0
mov cx,6
ClearDump:
mov [si][bx],al
inc bx
loop ClearDump

;字符输入循环控制部分
CharInputControl:
;16号中断获取字符
mov ah,0
int 16h

;判断是否是数字
;判断是否小于0
cmp al,'0'
jnb next

;非数字则判断是否为控制字符
jmp ControlCharInput 
;判断是否大于9
next:
cmp al,'9'

;数字则执行数字的输入
jna NumberCharInput

;非数字则判断是否为控制字符
jmp ControlCharInput

;控制字符判断
ControlCharInput:
;判断是否为左删除键
cmp al,08h
je BackSpace

;判断是否为回车
cmp al,0dh
je SetTimeNow

;判断是否为ESC
cmp al,1bh
je ReturnPointLong

;返回字符输入
jmp CharInputControl

ReturnPointLong:
jmp ReturnPoint

;是有效的数字输入
NumberCharInput:

;判断是否超过6个数字的输入
mov dx,[si-2]
cmp dx,5
ja  SetTimeNow

;数字写入相应得内存区域
mov ah,0
;程序超过CALL最大长度,要使用CALL far PRT或者CALL near PRT 实现跳转,
;但是这样长跳并不能解决所有问题。
;因为长跳是CS IP入栈 并重新设置编译器计算的地址,而本程序的地址不能通过编译器计算,
;此时的call 和JMP 有很大的差别.
;只能实行近跳(Near)
;注意JUMP FAR PTR 和jump near ptr 的区别很大,尤其在静态地址分配问题上.
call near ptr CharactorInputControl

;显示刚才的输入数字
mov ah,2
mov dh,9
mov dl,11
Call near ptr CharactorInputControl
;返回字符输入
jmp CharInputControl

;处理左删除
BackSpace:

;字符出栈
mov ah,1
call near ptr CharactorInputControl
;屏幕显示出栈后剩余的字符
mov ah,2
mov dh,9
mov dl,11
call near ptr CharactorInputControl
;返回字符输入
jmp CharInputControl

;时间设置开始
SetTimeNow:

;设置偏移
mov si,(TimeShowBuffer-T+800H)
mov bx,si
call near ptr ChangeFromKeyboardInput

mov si,(TimeShowBuffer-T+800H)
mov bx,(TimeBuffer-T+800H)
call near ptr ChangeToBCD

mov si,(TimeBuffer-T+800H)
call near ptr WriteToCMOS

jmp ReturnPoint

;屏幕显示子程序,以'$'为结束标志
;以ds:si指向要显示的内存区,dh存放行,dl存放列
Show_Str proc near

;寄存器入栈保护
push es
push ax

push bx
push cx

mov ax,0b800h
mov es,ax
mov al,160
mul dh
mov bx,ax
mov dh,0
add dl,dl
add bx,dx
mov cx,bx ;为换行标志准备
LoopShow_Str:
;判断结束标志
cmp ds:[si],byte ptr '$'
je EndLoopShow_Str
;判断换行标志
cmp byte ptr ds:[si], 0ah
jne NoLF

add cx,160
mov bx,cx 
inc si
jmp LoopShow_Str

NoLF:
mov al,ds:[si]
mov es:[bx],al
add bx,2
inc si
jmp LoopShow_Str

EndLoopShow_Str:
pop cx
pop bx
pop ax
pop es
ret

Show_Str endp

;BCD码向十进制转换得子程序
;ds:[bx]指向要处理的数据的内存地址
;es:[si]指向处理完成后数据存放的地址
;'$'指示处理内存的数据的终止符号。
BCDChange proc near
;寄存器保护
push ax
push cx

BCDChangeStart:
mov al,ds:[bx]
cmp al,'e'     ;'$'的ASCII为36 即 0x24 会出现错误 e的ASCII为101 即 0x65   
               ;判断是否为终止符号
je ExitBCDChange

mov ah,al   ;保护AL
mov cl,4    ;设置移动位数
;高四位,并存储
shr ah,cl
mov es:[si],ah
inc si

;低四位
;清高四位
shl al,cl
shr al,cl

mov es:[si],al
inc si
inc bx
jmp BCDChangeStart

ExitBCDChange:

pop cx
pop ax
ret
BCDChange Endp

;数字向屏幕显示转化子程序
;ds:[bx]指向要处理的数据的内存地址
;es:[si]指向处理完成后数据存放的地址
;'$'指示处理内存的数据的终止符号。
ChangeForShow proc near
push ax

ChangeForShowStart:
mov al,ds:[bx]
cmp al,'$'
je ExitChangeForShow
add al,30h
mov es:[si],al
inc bx
inc si
jmp ChangeForShowStart

ExitChangeForShow:
pop ax
ret
ChangeForShow  Endp

;时间读取子程序
;ds:[bx]指向存放数据的内存地址
ReadTime proc near
push ax
;时
mov al,4
out 70h,al
in al,71h
mov ds:[bx],al 
inc bx
;分
mov al,2
out 70h,al
in al,71h
mov ds:[bx],al
inc bx
;秒
mov al,0
out 70h,al
in al,71h
mov ds:[bx],al
pop ax
ret
ReadTime Endp

;清屏子程序
ClearScrean proc near
push ax
push bx
push cx
push ds
mov ax,0b800h
mov ds,ax
mov cx,2000
mov bx,0
loopClearScrean:
mov ds:[bx],byte ptr ' '
add bx,2
loop loopClearScrean
pop ds
pop cx
pop bx
pop ax
ret 
ClearScrean Endp

;转化用户的键盘输入到实际的数据输入的子程序
;ds:[si]指向要转化的内存区于
;ES:[bx]指向转化完成后的内存地址
;用'$'作为结束的标志
ChangeFromKeyboardInput proc near
push ax
StartChangeFromKeyboardInput:
mov al,ds:[si]
cmp al,'$'
je ExitChangeFromKeyboardInput
sub al,30h
mov es:[bx],al
inc si
inc bx
jmp StartChangeFromKeyboardInput
ExitChangeFromKeyboardInput:
pop ax

ret
ChangeFromKeyboardInput Endp

;十进制向BCD码转化的子程序
;ds:[si]指向要处理的内存区域用'$'作为内存结束标志
;es:[bx]指向处理完的内存区域
ChangeToBCD proc near

push cx
push ax
mov cl,4
StartChangeToBCD:
mov al,ds:[si]
cmp al,'$'
je ExitChangeToBCD
shl al,cl
inc si
mov ah,ds:[si]
add al,ah
inc si
mov es:[bx],al
inc bx
jmp StartChangeToBCD
ExitChangeToBCD:
pop ax
pop cx
ret

ChangeToBCD Endp

;数据输入和屏显子程序
;ah存放功能号,0表示入栈,1表示出栈,2表示显示
;ds:[si]指向字符栈空间
;ds:[si]的前面两个字节用于存放计数器的数值
;对于0号功能:(al)=入栈字符
;对于1号功能:(al)=返回的字符
;对于2号功能:(dh)、(dl)=字符在屏幕上显示的行、列位置
CharactorInputControl proc near
push bx
push dx
push di
push es

cmp ah,0
je charpush
cmp ah,1
je charpop
cmp ah,2
je charshow
jmp sret

charpush:
mov bx,[si-2]
mov [si][bx],al
inc word ptr [si-2]
jmp sret

charpop:
cmp word ptr [si-2],0
je sret
dec word ptr [si-2]
mov bx,[si-2]
mov al,[si][bx]
mov byte ptr [si][bx],0  ;清除数据
jmp sret

charshow:
mov bx,0b800h
mov es,bx
mov al,160
mov ah,0
mul dh
mov di,ax
add dl,dl
mov dh,0
add di,dx
mov bx,0

charshows:
cmp bx,[si-2]
jne noempty
mov byte ptr es:[di],' '
jmp sret
noempty:
mov al,[si][bx]
mov es:[di],al
mov byte ptr es:[di+2],' '
inc bx
add di,2
jmp charshows

sret:
pop es
pop di
pop dx
pop bx
ret

CharactorInputControl Endp

;数据写入CMOS
;ds:[si]指向内存区域
;CPU速度太快,端口来不及取走数据,因此,必须设置CPU等待。
;不等待的话,总是设置错误,调了好几天才注意到.
;究竟等待多长,不清楚,因此干脆写入一个数据后,就一直从端口读出,和写入的比较,直到相同了,
;才设置下一个.
;数据从端口的读入读出时间正好使得CMOS有时间取走数据。
WriteToCMOS proc near
push ax
;时
mov al,4
out 70h,al
mov al,ds:[si]
mov ah,al
out 71h,al

;延时代码,CPU从端口取走数据会自动匹配端口速度,因此下列代码相当于延时,同时又提高了
;程序的执行速度和代码的简化
in al,71h
inc si

;分
mov al,2
out 70h,al
mov al,ds:[si]
mov ah,al
out 71h,al
inc si

in al,71h

;秒
mov al,0
out 70h,al
mov al,ds:[si]
out 71h,al
in al,71h
pop ax
ret
WriteToCmos Endp

Reboot:

mov ax,0ffffh
mov bx,0
push ax
push bx
retf ;retf 相当于
     ;pop ip 
     ;pop cs 
     ;cs ip 的值不能通过普通的mov指令修改

StartSystem:

;读取C盘的第一扇区到内存的0:7c00处

             mov bx,7c00h  ;读到内存中的地址。

             mov dl,80h
             mov ah,2     ;读磁盘操作     
             mov dh,0     ;0盘面
             mov ch,0     ;0磁道
             mov cl,1     ;1扇区
             mov al,1     ;只读一个扇区
             int 13h

;置CS,IP
mov ax,0
mov bx,7c00h
push ax
push bx
retf ;retf 相当于
     ;pop ip 
     ;pop cs 
     ;cs ip 的值不能通过普通的mov指令修改

;计算代码长度
ProgrameEnd:

Code ends

end Start

附上BIOS内存分布图
386工作在实模式下内存情况分布图