作者:K_Linux_Man 转载请注明出处
无论是汇编语言,还是C语言第一个程序都是输出“Hello, World”,但是我们在制作操作系统之前一定要给自己的操作系统起一个了不起的名字。
而我给它命名为“MerxOs”.
让我们的MerxOs操作系统在开机的时候能够显示“Hello,World”是我们接下来要做的事情,然后我们必须要知道如何让屏幕去显示字符,这个看起来简单的问题。
BIOS中断---显示字符
在BIOS中断里,int 10中断是显示中断。在10号中断里有很多功能,我们即将要使用显示字符到屏幕的功能去实现我们的引导扇区程序显示“Hello,World”字符串。
寄存器 | 赋值 | 描述 |
AH | 0x0E | 给AL赋值0x0E代表使用显示字符功能 |
AL | 字符 | 将字符赋值于AL |
BH | 页码值 | BH=0x0 代表当前页 |
BL | 显示的属性 | 0xF 代表 黑底,白字 (0000 1111) |
引导扇区格式
BIOS不是随便就去加载一个扇区的,所以引导扇区必然有规定的格式。这个扇区必须在第一个扇区的位置,并且以55AA结尾。所以,我们写完引导扇区后,
要填充0直至510字节,然后填充55AA。前面我们讲过这个扇区会被加载到7c00h的位置去执行,所以引导扇区程序的第一行代码是org 0x7c00h。
引导扇区程序的编写
编写代码我使用NotePad++
将语言调整到Assembly格式,以下截图
512K的引导程序代码如下:
org 0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov si,String
call ShowString
jmp $
String: db 'MerxOs: Hello, World!',0
ShowString:
mov ah,0x0E
mov al,[si]
inc si ; mov al,[si] 和 inc si 可以用一条指令完成。lodsb
or al,al
jz .end
mov bh,0x00
mov bl,0x0F
int 10h
jmp ShowString
.end:
ret
times 510-($-$$) db 0;(剩余代码填充0)当前汇编地址减去起始地址=代码段的数据长度,512减去代码段长度等于剩余代码长度
db 0x55
db 0xAA
引导程序的实施
nasm编译汇编代码
找到nasm安装的根路径,默认在C:\cygwin。这个目录下是模拟linux下的目录,我们在home\administrator(以自己计算机名称命名的文件夹,根据自己的计算机名,
有不同的命名)目录下新建一个文件夹,命名为:first_code。将我们的汇编代码保存到first_code目录下,并命名为boot.asm
打开 cygwin之后,ls查看一下咱们刚才新建的文件夹first_code,再进入查看一下咱们的引导程序代码boot.asm
接下来我们要用nasm编译器编译我们的boot.asm 引导程序,并命名为boot.img
命令为: nasm boot.asm -o boot.img
我们已经成功的生成了引导程序的镜像文件。接下来我们要将此镜像文件以软盘镜像的文件类型让bochs加载并显示处我们的"MerxOs:Hello,World!"内容
Bochs运行软盘镜像
到bochs的根目录下,默认安装路径为C:\Program Files\Bochs-2.5
找到dlinux文件夹,复制一份并修改成咱们自己的操作系统名称:MerxOs
修改MersOs 文件夹下 run.bat 和 bochsrc.bxrc 的内容。
run.bat 是一个批处理文件,而bochsrc.bxrc是bochs的配置文件。
run.bat 只要修改一下路径就可以了。将 "C:\Program Files\Bochs-2.5\dlxlinux" 修改为"C:\Program Files\Bochs-2.5\MerxOs"
bochsrc.bxrc 修改的地方也不多。
1.将floppya.img 修改成咱们自己生成的boot.img名字。
2.将硬盘主分区那行用#注释掉,意思是不从硬盘加载MBR。
3.改为boot:a 从软盘启动
将我们生成的boot.img 文件拷贝到MerxOs 文件夹中,运行run.bat即可。
运行结果:
Bochs 强大的调试功能
在MerxOs 文件夹中是没有bochsdbg.exe(bochs的debug程序).它是从上一级目录考取过来的。
双击运行bochsdbg.exe,即可进入debug调试模式。
点击strat,即选择从默认的bochsrc.bxrc配置文件开始调试。
在0x7c00处加入一个断点,然后继续执行到0x7c00处停止。
逐条执行代码: Step 1 或 s 1
反汇编10行代码:u/10
查看寄存器内容:reg
查看段寄存器内容:sreg
继续执行: continue 或 c
引导程序代码的解释:
ORG 0x7c00的作用
org 0x7c00 是告诉编译器将程序加载到内存的什么位置。你可能有一个疑问? “BIOS不是将引导程序主动加载到内存地址为0x7c00的地方吗,
干嘛还要在汇编程序告诉编译器加载地址呢?!”我一开始的时候就没有加org这行代码,不信的话你注释掉这行代码再去运行的话,不会显示
咱们要的内容。这是为什么呢?通过bochs调试功能可以看出(接下来会有怎么用bochs调试代码的内容)。
关键在于运行到mov si, String 这样代码的时候。加上org 0x7c00的时候,直接将String翻译成0x7c0e,String本应该是相对这段程序开始的偏移量。加上org 0x7c00的话,
String的地址(0x7c0e)是由0x7c00和相对于这段程序的偏移量(0xe)相加而得.
如果不加 org 0x7c00的话,String会被翻译成相对于程序开始的偏移地址 0xf,为什么不是0xe呢? 这是因为 mov ax, 0x7c0,翻译成机器语言是三个字节,相比原先代码中mov ax,cs 是两个字节,所以String地址变成了0xf.
当程序运行到 mov al,ds:[si] ,也就是说si是数据段寄存器的偏移值才得到咱们定义的字符串“Hello,World”,而ds的内容是什么呢?等于cs的内容,
而cs的内容是0x00。所以mov al,0000:[si] 是相对0x00开始的si偏移,而那里根本不是我们存放字符串的地址。正确的地址是相对0x7c0偏移si的地址.
不过,即使你不加org 0x7c00这个地址,而主动告诉ds数据段寄存器应该存放0x7c0地址,也是可以的,而代码段cs寄存器内容还是0x00.只是数据段寄存器变为0x7c0了。(0x7c0:si)所以,如下代码也能达到相同效果。这里必须说明
一下,ds赋值的内容是0x7c0,而不是0x7c00。原因是由于寻址方式,内存地址=段*16+偏移地址,所以是0x7c0,段寄存器需左移一位+偏移地址。
;org 0x7c00
mov ax, 0x7C0 ; 地址=段*16+偏移的定义,所以是0x7c0,而不是0x7c00,段寄存器需左移一位
mov ds, ax
mov es, ax
mov si,String
call ShowString
jmp $
String: db 'MerxOs: Hello, World!',0
ShowString:
mov ah,0x0E
mov al,[si]
inc si
or al,al
jz .end
mov bh,0x00
mov bl,0x0F
int 10h
jmp ShowString
.end:
ret
times 510-($-$$) db 0;(剩余代码填充0)当前汇编地址减去起始地址=代码段的数据长度,512减去代码段长度等于剩余代码长度
db 0x55
db 0xAA
定义字符串
String: db 'MerxOs: Hello, World!',0 这里是单引号,最后以0结尾,在内存中定义了一个以0结尾的字符串。String代表字符串相对于该程序的起始偏移地址
其他
对字符串中每个字符依次显示,直到遇到0为止结束循环。这里说明一下 mov al,[si] 是将si这个地址里存放的内容赋值给al
最后, $$代表这段代码进行汇编时的起始地址,而$是当前被编译代码的地址,他们两个的差值,正好是代码的大小。而我们要做的是
将最后两个字节赋值为0x55和0xAA,而其余的全部填充为0.
执行控制指令
c/cont/continue | 连续执行 |
s/step/stepi [count] | 执行count条指令,默认为1条,会跟进到函数和中断调用的内部 |
p/n/next [count] | 执行count条指令,默认为1条,但跳过函数和中断调用 |
Ctrl+C | 停止执行,并回到命令行提示符下 |
q/quit/exit | 退出调试和执行 |
断点设置命令
vb/vbreak seg:offset | 在虚拟地址上设置指令断点,其中seg和offset可以是以0x开始的十六进制数,或十进制,或者是以0开头的八进制数 |
lb/lbreak addr | 在线性地址上设置断点,addr同上面的seg和offset |
b/break/pb/pbreak addr | 在物理地址上设置断点 |
info break | 显示当前所有断点的信息 |
d/del/delete n | 删除一个断点 |
内存操作指令
x /nuf addr | 检查位于线性地址addr处的内存内容 |
xp /nuf addr | 检查位于物理地址addr处的内存内容 |
其中参数n、u、f分别表示:
n为要显示内存单元的计数值,默认为1
u表示单元大小,默认值为w
b(bytes) 1字节
h(halfwords) 2字节
w(words) 4字节
g(giantwords) 8字节
f为显示格式,默认为x
x(hex) 显示为十六进制数
d(decimal) 显示为十进制数
u(unsigned) 显示为无符号十进制数
o(octal) 显示为八进制数
t(binary) 显示为二进制数
c(char) 显示为对应的字符
信息显示命令
r/reg/regs/registers | 列表显示CPU寄存器及其内容 |
set $reg=val | 修改某寄存器的内容。除段寄存器和标志寄存器以外的寄存器都可以修改,如set $eax=0x01234567 |
creg | 列出所有的CR0-CR4寄存器 |
sreg | 列出CPU全部状态信息,包括各个段选择子(cs,ds等)以及ldtr和gdtr等。 |
print-stack | 打印堆栈情况。 |
info tab | 显示页表 |
反汇编命令
u/disasm/disassemble start end,反汇编给定线性地址范围的指令。也可以是u /10 反汇编从当前地址开始的10条指令。