bits 16 ; 16 bit real mode
org 0x7C00 ; loader start in memory
start: jmp main ; goto main
bgetkey: mov ax, 0 ; clear register a
mov ah, 0x10 ;
int 16h ; interrupt bios keyboard
ret ; return
.buf dw 0 ; buffer size one word
prints: pusha ;
.loop: mov ah, 0x0e ;
mov al, [si] ;
cmp al, 0 ; check for null terminator
jz print_end ; stop printing
mov bh, 0x00 ;
mov bl, 0x07 ;
int 0x10 ; interrupt bios tty
inc si ; next character
jmp .loop ; jump beginning
print_end: popa ;
ret ; return
main: mov ax, 0x0000 ; clear register a
mov ds, ax ;
mov si, welcome ; copy welcome string pointer
call prints ; print string
newinput: mov bx, mem ; set register b to memory start
add bx, word 2 ; increment by size of memory ptr
mov word [mem], bx ; set pointer at first memory byte
type: mov si, qbuf ; set byte buffer ptr for printing
call bgetkey ; capture keyboard input
mov [qbuf], al ; copy key byte to buffer
call prints ; print the character
mov bx, [mem] ; copy memory stop to b, decrement
cmp bx, stop - 1 ; check for overflow preserve null
je oom ; halt - no more memory
mov byte [bx], al ; copy keystroke to memory
add bx, byte 1 ; increment memory pointer
mov [mem], bx ; restore memory pointer to memory
cmp byte [qbuf], 0x0D; check for carriage return
jne type ; goto next key if not found
mov si, newline ; copy pointer to line feed string
call prints ; print the line feed
mov bx, mem + 2 ; restore start of valid memory
readmem: cmp byte [bx], 0x0D ; check for carriage return
je readmemdone ; if found begin another input line
mov cl, [bx] ; register character byte
mov byte [qbuf], cl ; copy byte to string buffer
mov si, qbuf ; copy buffer ptr for printing
call prints ; print the character
inc bx ; increment memory pointer
jmp readmem ; read another character byte
readmemdone: mov byte [qbuf], 0x0D; copy carriage return to buffer
call prints ; print carriage return
mov si, newline ; copy line feed string to buffer
call prints ; print line feed
jmp newinput ; ready new line
oom: mov si, outomem ; copy out of memory message ptr
call prints ; print message
halt: mov si, halting ; copy halting mesage ptr
call prints ; print final message
hlt ; halt the cpu
welcome db "boot", 0x0A, 0x0D, 0x00
newline db 0x0A, 0x00
outomem db 0x0A, 0x0D, "out of memory", 0x0A, 0x0D, 0x00
halting db 0x0A, 0x0D, "halting", 0x00
qbuf dw 0, 0
mem db 0
times 0200h - 2 - ($ - $$)db 0
stop dw 0xAA55
我使用脚本来编译和运行该程序。每次都会对所有文件进行后备备份:
nasm -o boot.bin -f bin boot.asm && \
tar -zcvf ~/os-$(date +%Y%m%d%H%M%S).tar.gz ../os && \
qemu boot.bin
如果我键入:
测试123
hello loader
溢出测试[按
.
直到停止] 我得到以下正确的输出:
在较早的时间点,我认为我永远不需要学习低级语言。我错了。一旦我了解了它们的作用,空白注释就是占位符。我主要是要求对评论的准确性提出反馈,其次是要寻求大小的效率以及整体流程和布局的正确性。
#1 楼
有关代码原始机制的大量评论。没有太多的设计审查。因此,这里只是一些设计技巧。使用BPB,因为其他人会这样做。
程序可以做到这一点。首先也是为了跳过嵌入式BIOS参数块。您可能不认为您的卷中需要BPB。您几乎可以肯定会。其他人则认为,多年来,后来在做诸如没有BPB的事情时被咬伤,并使用要求各种分区在VBR中具有BPB的操作系统中的工具。它在OS / 2引导管理器中咬住了IBM。
第二个是,甚至有一些工具(损坏的工具,幸运的是不再广泛使用),可以使VBR中的第一条指令无效,期望它是
jmp
。您的代码是否会在1979年生产的原始8088上运行? >
但不是出于所述原因。它们阻止了仅在
cli
的一半已加载的窗口中发生中断,因此堆栈指针无效。即使没有伴随这些其他段加载的偏移量,也没有必要将这808x智慧错误地扩展到该答案中的其他段寄存器。实际上,中断处理程序不希望其他寄存器具有任何特定值,并且您无需临时禁用中断即可对其进行操作。 8086“ far”内存模型DOS程序始终可以在不屏蔽中断的情况下操作sti
和ss:sp
。而且重要性被夸大了。自8088年以来,一个ds
隐含地导致中断(甚至es
无法屏蔽的中断)延迟一条指令,因此一个人可以在它之后立即插入mov ss
,而无需显式cli
,两条指令将隐含中断安全/ mov sp
对。 (早期的8088s有一个错误,但是8088参考手册将其记录为这种行为。今天,它仍然存在于Intel参考手册中。)从80386开始,我们有了cli
指令,该指令将寄存器对装入一条指令。 /> 有趣的历史观点
在1987年的PC Magazine中,罗伯特·L·胡默尔(Robert L. Hummel)称8088错误为“应该引起的严重错误”,并解释了(当时已经是民俗的)解决方法
sti
和lss esp
配对中的一个。实际上,英特尔已在1981年修复了该错误。80386于1985年问世。三十年后,人们仍在遵循民间规则来编码这些东西,甚至变得失真了。正确退出。
start: jmp main ; goto main
恭喜!您刚刚执行了一个提示字符串作为代码。
IBM PC兼容固件提供了两个软件中断,用于正确地将引导程序“退出”到固件。 BIOS引导规范说明了它们的用法。
进一步阅读
Jonathan de Boyne Pollard(2006)PC / AT引导过程。常见问题解答。
乔纳森·德·博因·波拉德(Jonathan de Boyne Pollard,2011年)。一个PC / AT风格的MBR引导程序,用于EFI分区光盘。软件。
乔纳森·德博因·波拉德(2006)。有关BIOS参数块的所有信息。
Jonathan de Boyne Pollard(2006)卷启动块中OEM名称字段的含义和使用。
Will Fastie(1983年9月/十月)。 “跟踪8088中的错误”。 PC技术笔记本电脑4。 106
Sergei Kiselev(2011)“堆栈更改竞赛条件”。历史记录。
Robert L. Hummel(1987-12-08)。 “ PC辅导员:8088芯片有缺陷”。 PC杂志。第6卷第21期.ISSN 0888-8507。齐夫-戴维斯。 p.492。
康柏计算机公司;凤凰科技有限公司;英特尔公司。 BIOS引导规范1996-01-11。
#2 楼
您与清除寄存器的方式非常不一致:bgetkey: mov ax, 0 ; clear register a
和
main: mov ax, 0x0000 ; clear register a
我建议
xor
ing 示例:
xor ax, ax
@icktoofay在本节中建议:
bgetkey: mov ax, 0 ; clear register a
mov ah, 0x10 ;
int 16h ; interrupt bios keyboard
ret
不必清除所有
ax
并单独更改ax
,只需执行以下操作:在一条指令中更改ax
的高字节和低字节。我发现在引导加载程序中很常见,因为人们发现它采用了“简便的方法”。相反,人们经常手动设置以下段:
请参阅bios内存映射(堆栈所在的位置):
mov ax, 0x1000
请注意,
org 0x7C00
和cli
非常重要。当处理段时,中断可能会“意外”触发,这可能会使您的程序混乱。 sti
将禁用中断,而cli
将重新启用中断。在这样的某些行上:认为
sti
是必要的,因为byte
的大小已经是一个字节了,尽管我尚未检验该理论。不知道它们是什么,而其他字符可能需要一点时间来处理。 /> 例如,在
cli
mov ax,07C0h
mov ds,ax
mov gs,ax
mov fs,ax
mov es,ax
例程中,即使al
不是,每次迭代都使用0x0A
恢复0x0D
。在例行程序中完全没有接触过。我建议放下
mov ax,07E0h
mov ss,ax
mov bp,ax
mov sp,0xff
sti
在
%define
标签之前,因此您不必要地更新prints
。 >我将继续浏览代码,并添加我认为必要的更多改进。评论
\ $ \ begingroup \ $
重新。建议用mov al,0替换mov ax,0:是的,但是我还要走得更远,如果您要更改高字节和低字节,请同时用mov ax更改它们, 0x1000。
\ $ \ endgroup \ $
–icktoofay
15年6月21日在2:24
\ $ \ begingroup \ $
@icktoofay啊,是的!这是一个好主意。您介意我修改我的帖子以适应您的建议吗?
\ $ \ endgroup \ $
– SirPython
2015年6月21日,下午2:31
\ $ \ begingroup \ $
请一定做!
\ $ \ endgroup \ $
–icktoofay
2015年6月21日,下午2:32
\ $ \ begingroup \ $
我看到的在该代码中禁用中断的唯一需求是MOV无法原子更新SS和SP。两次更新之间的中断将使用不一致的堆栈指针。无需禁用中断就可以安全地完成其他段寄存器的更新。
\ $ \ endgroup \ $
–卡巴斯德
15年6月21日在16:50
\ $ \ begingroup \ $
@SirPython我实际上已经忘记了它是0000:7C00 ...已经很久了。但是,我的主要观点是,在OP希望使用的任何硬件(包括VM)上,CS为0000,而不是07C0。自8086年代初以来,它就没有被破坏过,因此,再也没有必要继续进行该代码了。这是一个剩余的“民俗”,人们在它的到期日期后很长时间一直在疏dr。如果要重新放置加载器,则最终需要跳远,但这不是因为寄存器不正确。
\ $ \ endgroup \ $
– phyrfox
15年6月23日在18:26
#3 楼
我看到了许多可以帮助您改进代码的方法。常量,例如2、0x0e,0x10等。通常最好避免这种情况,并为此类常量赋予有意义的名称。这样,如果需要更改任何内容,则无需遍历代码中的所有“ 7”实例,然后尝试确定此特定的0x07是否与所需的更改相关,或者是否与其他更改有关具有相同值的常数。使用NASM,可以使用%define
指令:%define KBDINT 16h
然后在代码中:
int KBDINT
使用
XOR
清除寄存器x86汇编语言中清除寄存器的惯用方式是使用
xor
:xor ax,ax ; ax = 0
此指令编码是比
mov ax,0000h
短。 使用注释指示寄存器使用情况
跟踪寄存器使用情况是汇编语言程序员最重要的任务之一。跟踪此内容的一种有用技术是注释的使用。例如,代替这个:
bgetkey: mov ax, 0 ; clear register a
mov ah, 0x10 ;
int KBDINT ; interrupt bios keyboard
ret ; return
编写这个: br />
尽管NASM中的宏支持不是很好,但它确实存在并且可以用来简化您的代码。例如,上面的例程仅被访问一次。上面的简化版本只有3条指令,但是只有2条指令,如果将
call
内嵌在代码中,则可以省去。我会这样写:;****************************************************************************
;
; bgetkey - use BIOS call to get a keystroke; blocks until key available
;
; INPUT: none
; OUTPUT: ah = BIOS scan code, al = ASCII char
; DESTROYED: none
;****************************************************************************
bgetkey:
mov ah, 0x10 ;
int KBDINT ; interrupt bios keyboard
ret ; return
然后在这样的代码中使用:
%macro BIOSWAITKEY 0
mov ah, 0x10 ;
int KBDINT ; interrupt bios keyboard
%endmacro
显式设置段寄存器
调用装载程序的BIOS应该将
CS
和DS
都设置为0
,但是很遗憾,不能保证。一些旧的BIOS会调用7C0:0
而不是0:7C00
,因此大多数引导加载程序代码都会明确设置段寄存器。您的代码仅设置DS
寄存器,而不设置SS
。对于健壮的引导程序,请将段寄存器显式设置为0
或等于CS
。仔细考虑堆栈使用情况
prints
例程当前先推送然后弹出所有寄存器。这个例程对速度没有严格要求,但是养成仔细考虑堆栈使用情况的习惯很有用。在这种情况下,我可能只会保存AX
和BX
或仅保存BX
。该代码实际上并不需要保留SI
的值,也不一定要保留AX
的值,只需对调用代码进行一些小的改动。维护代码与标签在同一行上的代码很麻烦。更好的做法是让每个标签自己排成一行。这使得维护代码更加容易。节省周期和时间。在这段代码中,我们有:type:
mov si, qbuf ; set byte buffer ptr for printing
BIOSWAITKEY
mov [qbuf], al ; copy key byte to buffer
这意味着循环末尾的无条件
jmp
总是被执行。相反,最好将代码重组为在循环内只有一个条件分支。将
AX
寄存器设置为靠近INT
指令为了使其他程序员更容易理解您的代码,最好在
AX
指令调用BIOS或操作系统功能之前设置AH
(或INT
寄存器)。这样,两个最重要的信息,即“哪个中断”和“哪个服务”彼此靠近,因此很容易查找它们。更好的是,使用命名常量并将它们彼此靠近。消除未使用的变量
永远不要使用
.buf
中的bgetkey
区域,应该将其删除。 优先选择运行时数学的汇编时间
newinput
标签以以下三行开头:prints: push ax ; modified per previous point
push bx ;
.loop: mov ah, 0x0e ;
mov al, [si] ;
cmp al, 0 ; check for null terminator
jz print_end ; stop printing
mov bh, 0x00 ;
mov bl, 0x07 ;
int 0x10 ; interrupt bios tty
inc si ; next character
jmp .loop ; jump beginning
print_end: pop bx ;
pop ax ;
ret ; return
更好而是让汇编程序来进行计算:也就是说,只需使用
mem
来存储指针并将所有bx
用作缓冲区。 在适当的地方简化调用机制
mem
结构似乎仅是为了打印单个字符输出而设置的。在代码中,将qbuf
的第一个字节设置为一个值,然后将qbuf
指向si
,然后调用qbuf
例程。最好通过创建一个例程来简单直接地打印单个字符来简化。实际上,这是BIOS视频TTY输出例程(您正在使用的)实际上已经在执行的操作,因此请为该函数创建一个函数:prints:
push ax ;
push bx ;
jmp .begin ; skip over loop first iteration
.loop:
mov bx, PAGE0WHTBLK ; page 0, white on black
mov ah, TTYOUT ;
int VIDINT ; interrupt bios tty
inc si ; next character
.begin:
mov al, [si] ;
cmp al, 0 ; check for null terminator
jnz .loop ; keep printing
pop bx ;
pop ax ;
ret ; return
现在,宏对于使用该例程非常有用: mov bx, mem ; set register b to memory start
add bx, word 2 ; increment by size of memory ptr
mov word [mem], bx ; set pointer at first memory byte
现在可以像以下任何一种方式使用它:
命名重要的存储位置
一个非常重要但未命名的存储位置是
prints
区域的末尾。我将修改现有代码以使其看起来像这样: mov bx, mem+2 ; point to available space
mov word [mem], bx ; save pointer to available space
充分利用每个字节
,特别是在引导加载程序中,每个字节都很重要,利用每个可用字节很有用。例如,在您的代码开始之后,不再需要“欢迎”消息。您可以将该消息覆盖在
mem
缓冲区中,以允许它被用户输入覆盖。同样,从技术上讲,一旦将扇区读入内存,签名mem
字节也可能会被覆盖。同样,错误消息字符串也可以合并以节省一些字节。但是典型的x86汇编语言代码在第1列中带有标签格式,并在第9列中缩进代码(即,一个传统制表位的大小)。之后,注释通常会在制表位的多个倍数处对齐(如您所愿)。如果将0xAA55
指向内存缓冲区,则可以避免保存di
寄存器。 注释应该说明原因,而不是原因
注释通常应该说明您为什么做自己的工作,而不是简单地重复说明。因此,这不是一个好评论:
;****************************************************************************
;
; printch: prints a single character to screen
;
; INPUT: al = character to print
; OUTPUT: none
; DESTROYED: none
;****************************************************************************
printch:
push ax ;
push bx ;
mov bx, PAGE0WHTBLK ; page 0, white on black
mov ah, TTYOUT ;
int VIDINT ; interrupt bios tty
pop bx ;
pop ax ;
ret ; return
这是一个更好的评论:
%macro PRINTCHAR 1
%ifnidni %1,al
mov al, %1
%endif
call printch
%endmacro
将所有内容放在一起
应用所有这些建议将产生一个易于维护,易于阅读,更小且结构更好的程序: >
PRINTCHAR al
PRINTCHAR [bx]
PRINTCHAR CR
使用真实版本控制,而不仅仅是文件副本
快速而实验性地更改源文件时,拥有备份文件固然重要,但您可以考虑为此使用更合适的机制。我建议您可能要使用
bx
而不是创建多个gzip文件。这样,即使您不打算共享代码,也更容易记录更改原因。#4 楼
我回覆了SirPython的大部分建议(可能的建议段设置代码除外,我更喜欢设置所有段寄存器为零的平面内存空间),但还有其他一些我可能要更改的地方。我应该注意,其中一些可能更多是关于品味的问题。十六进制常量
您与十六进制常量的表示法不一致。您在某些地方使用
0xABCD
语法,而在其他地方使用0ABCDh
语法。我更喜欢0ABCDh
语法,但这只是我一个。(本质上)使用
.data
,其中.bss
足够了将行缓冲区放入分配给引导加载程序的空间中。在编写代码时,可用空间将减少。当然,如果您需要将某些空间初始化为某些特定的初始值,那么这很方便,因此应该将其保留在其中,但是如果您不需要将其初始化为特别的任何东西,则无需将其填充到那些初始值中512字节您几乎可以使用整个地址空间。 (从
500h
开始,直到您自己敲击引导程序代码,然后直到……gee,还有其他事情。但是您还有很多空间。)但是请记住,如果您希望它为零,已初始化,现在您必须自己对其进行零初始化。字符串指令
我看到很多代码访问缓冲区,对其进行处理,然后递增该指针。帮自己一个忙,阅读x86体系结构提供给您的字符串指令:尤其是
lodsb
和stosb
可能会成为您最好的朋友。inc
几乎不需要
add reg, 1
。节省空间并使用inc reg
。 (使用add
时,您需要空间来存储1
。对于inc
则不是。)还有dec
会递减,如果以后需要的话。同样,考虑使用test reg, reg
而不是cmp reg, 0
。同样,这避免了立即存储0
的需要。 (这也是为什么当xor reg, reg
大于一个字节时,mov reg, 0
优于reg
的原因。)在这里,是样式问题。当我使用局部点标签时,通常会缩进它们以表示某种结构,例如:标签,所以我会像处理其他任何标签一样冲洗掉剩下的标签。删除例如
db
上的操作数大小提示。在某些地方您无法删除它们(最常见的情况是在使用带有内存操作数和立即数操作的指令时使用),但是在其他情况下,我认为它很混乱。选择汇编器
这完全是个人喜好,但是我使用了一段时间的NASM,直到遇到了我认为是一个非常奇怪的错误,该错误开始在16和20 32位边界。当我遇到该问题时,我切换到FASM(语法非常相似)。这解决了我的问题,从那以后,我变得越来越喜欢它,只是因为它确实造成了一些语法上的差异。您可能也想尝试一下。
1 FASM所做的NASM没有做的一件事是它会记住您声明数据定义是哪种类型。因此,例如:
read_line:
.next_character:
.done:
...将起作用;
dw
将是一个字节-mov byte [bx], al
,因为mov
被定义为字节。 (当然,可以使用指定的显式操作数大小来覆盖。)FASM的宏样式我也认为比NASM更好,FASM的mov
非常适合在程序外部布局内存(如行缓冲区)。 评论
\ $ \ begingroup \ $
认识lodsb和stosb(以及scasb)和朋友是件好事,但也值得一提的是,它们不一定比相应的等效说明要快。在上下文中进行测试是了解的最佳方法。
\ $ \ endgroup \ $
–爱德华
15年6月22日在23:50
\ $ \ begingroup \ $
@Edward:我不建议您考虑速度,而建议考虑代码大小,因为引导加载程序中的空间非常宝贵。另外,它们非常方便。
\ $ \ endgroup \ $
–icktoofay
2015年6月25日在1:39
#5 楼
SyrPython和icktoofay的所有内容,再加上...四个指令:
mov ah, 0x0e ;
mov al, [si] ;
cmp al, 0 ; check for null terminator
jz print_end ; stop printing
如果我理解正确(那时候我使用了另一个汇编程序。我想是Borland) mem在bx中的偏移量。那为什么不这样做
mov ax,0x0e00 ; clears al
or al,[si] ; sets Z accordingly (if [si] is 0)
jz print_end ; stop printing
就像您在其他地方一样?如果您使用建议的lodsb,则不会使用,可能会或可能不会与rep一起使用,取决于您的未来方向)
您有8个寄存器Ax Cx Dx Bx SP BP SI DI SP,您通常不会玩
SI和DI通常用于偏移量(将BX用于什么)
DX用于其他数据
BP主要用于堆栈框架,而您并不在乎,另一个免费的堆栈框架。 ,reg,这的确是一种首选方法,它唯一的缺点是会影响标志,因此在对代码进行流水线化(将操作按并行执行的顺序放置)时,不能很好地使用它> ie:
newinput: mov bx, mem ; set register b to memory start
add bx, word 2 ; increment by size of memory ptr
cmp将在管道U中执行,而mov在管道V中执行,因为它不影响cmp操作。
但这可能对装载程序来说有点过头了。
评论
\ $ \ begingroup \ $
在这种情况下,我同意的U / V位居首位,但是当他们尝试优化视频硬件时,这些信息肯定很不错。
\ $ \ endgroup \ $
– phyrfox
15年6月22日在19:43
评论
\ $ \ begingroup \ $
除了BIOS引导规范没有描述如何从引导加载程序退回到固件之外,都是好的建议。相反,它描述的是BIOS的功能,而不是引导程序需要执行的操作。我还将提供此真实的引导程序源代码作为灵感。
\ $ \ endgroup \ $
–爱德华
15年6月21日在14:58
\ $ \ begingroup \ $
您尚未正确阅读。仔细阅读所有内容,并特别注意附录D。
\ $ \ endgroup \ $
– JdeBP
2015年6月21日15:06
\ $ \ begingroup \ $
我实际上对它很熟悉-撰写本文时,我在Compaq从事BIOS开发。它表示如果操作系统不存在或无法加载,则应执行int 18h。在此引导加载程序中,这两个条件都不成立,因此建议不适用。
\ $ \ endgroup \ $
–爱德华
15年6月21日在15:12