今天开始学习自制操作系统课程,任务是搭建实验环境和简单了解汇编基础语法。 本次使用环境版本如下:
- Ubuntu 23.10
- VScode 1.84.2
- VirtualBox 7.0.6
如果使用Windows环境建议安装msys2或WSL,以便在Windows中使用Linux命令工具。
安装nasm汇编工具
# ubuntu安装nasm汇编编译器
sudo apt install nasm
# 也可以在Windows+msys2环境中使用pacman包管理器安装
# pacman -S nasm
nasm -v
# 显示版本表示安装完成
NASM version 2.16.01
创建项目
mkdir -p ~/Code/myOSCode
cd ~/Code/myOSCode
touch hello.asm
# vscode 打开编辑编辑如下汇编程序
code hello.asm
org 07c00h ; 告诉编译器程序将装载至0x7c00处
mov ax, cs
mov ds, ax
mov es, ax ; 将ds es设置为cs的值(因为此时字符串存在代码段内)
call DispStr ; 显示字符函数
jmp $ ; 死循环
DispStr:
mov ax, BootMessage
mov bp, ax ; es前面设置过了,所以此处的bp就是串地址
mov cx, 16 ; 字符串长度
mov ax, 01301h ; 显示模式
mov bx, 000ch ; 显示属性
mov dl, 0 ; 显示坐标(这里只设置列因为行固定是0)
int 10h ; 显示
ret
BootMessage: db "Hello, OS world!"
times 510 - ($ - $$) db 0 ;前510字节减去被使用的空间,剩余空间写入0
db 0x55, 0xaa ; 确保最后两个字节是0x55AA,才能被识别为可启动文件
这是一段简单的系统启动程序,占用磁盘的前512字节,功能是在屏幕上输出"Hello,OS World!"
编译程序并制作启动盘
# 将源文件编译为二进制程序
nasm hello.asm -o hello.bin
mkdir -p ~/VM/myOS/demo1/
# 使用dd命令将程序创建虚拟软盘并写入大小为512字节的启动程序
dd if=~/Code/myOSCode/hello.bin of=~/VM/myOS/demo1/mydisk.img bs=512 count=1
使用VirtualBox注册该软盘
- 找到虚拟软盘的路径并完成注册。
创建虚拟机
- 系统类型为Other其他
- 内存大小为64M以内
- 不添加磁盘,之后再设置。
- 再设置->存储->添加控制器->I82078(软盘)
- 在软盘控制器中添加注册好的软盘
启动虚拟机显示Hello, OS World!
汇编
由于操作的系统的启动程序都是由汇编语言实现的,所以需要掌握基础的汇编知识。 汇编语言与CPU指令集是对应的,不同指令的CPU的汇编语言也不同。 目前主流架构的CPU有:
- X86架构 1978年诞生于英特尔,之后由英特尔和AMD共同研发,是复杂指令集架构CPU,分为x86(32位)和amd64(64位),目前还是PC主流架构。
- ARM架构 1985年ARM项目正式启动,它一款低功耗精简指令集(RISC)的CPU,目前是移动端的主流架构。
- Power架构 1990年由IBM研发的精简指令集(RISC)架构,主要用于大型服务器和超算。
- RISC-V架构 RISC-V(发音为“risk-five”)是一个基于精简指令集(RISC)开源指令集架构(ISA)。该项目2010年始于加州大学伯克利分校。
- LoongArch架构 2020年,龙芯中科基于二十年的CPU研制和生态建设积累推出了龙架构(LoongArch™) 龙芯架构 LoongArch 是一种精简指令集计算机(RISC)风格的。龙芯架构分为 32 位和 64 位两个版本,分别称为 LA32 架构和 LA64 架构。
x86汇编指令
在汇编中,x86指令通常的助记符形式为:
# Intel语法表示的指令如下,目的操作数在前面:
助记符 目标地址,源地址
mov ax,cs ;将段寄存器cs中的值赋值给累加器ax
助记符是人类可读的机器指令表示,源地址和目标地址是指令的操作数。如汇编指令mov rbx,rax就是将寄存器rax的值赋给rbx。注意并非所有的指令都有两个操作数,有些指令甚至没有操作数。
常见的x86指令
数据传输
- mov dst,src ;将src赋给dst
- xchg dst1,dst2 ;互换dst1和dst2
- push src ;将src压栈,并递减rsp
- pop dst ;出栈赋给dst,并递增rsp
算术
- add dst, src ;dst +=src
- sub dst, src ;dst –= src
- inc dst ;dst += 1
- dec dst ;dst –= 1
- neg dst ;dst = –dst
- cmp src1, src2 ;根据src1−src2设置状态标志位
逻辑/按位
- and dst, src ;dst &= src
- or dst, src ;dst |= src
- xor dst, src ;dst ˆ= src
- not dst ;dst = ~dst
- test src1, src2 ;根据src1 & src2设置状态标志位
无条件分支
- jmp addr ;跳转到地址
- call addr ;压入返回地址到栈上,然后调用函数地址
- ret ;从栈上弹出返回地址,然后跳转到该地址
- syscall ;进入内核执行系统调用
- 跳转分支(基于状态标志位)jcc addr仅在条件cc成立时才跳转到该地址,否则进入jncc相反条件,在条件cc不成立时跳转
- je addr / jz addr ;如果设置ZF零标志位则跳转(如当上一个cmp中的操作数相同时)
- ja addr ;上一次比较中,如果dst大于src则跳转(无符号)
- jb addr ;上一次比较中,如果dst小于src则跳转(无符号)
- jg addr ;上一次比较中,如果dst大于src则跳转(有符号)
- jl addr ;上一次比较中,如果dst小于src则跳转(有符号)
- jge addr ;上一次比较中,如果dst大于等于src则跳转(有符号)
- jle addr ;上一次比较中,如果dst小于等于src则跳转(有符号)
- js addr ;上一次比较中,如果结果为负则跳转,符号位置1
杂项
- lea dst, src ;将内存地址加载到dst中,(dst=&src,其中src必须在内存)
- nop ;空指令,不执行操作(用作代码填充)
首先,需要注意mov这个词并不准确,因为从技术上来讲不是将源操作数移动到目的操作数,而是对其进行复制,但是源操作数保持不变。其次,关于栈管理和函数调用,push和pop指令具有特殊意义。
通用寄存器
- AX,BX,CX,DX称作为数据寄存器 AX (Accumulator):累加寄存器,也称之为累加器;
- BX (Base):基地址寄存器;
- CX (Count):计数器寄存器;
- DX (Data):数据寄存器;
- SP 和 BP又称作为指针寄存器 SP (Stack Pointer):堆栈指针寄存器;
- BP (Base Pointer):基指针寄存器;
- SI 和 DI又称作为变址寄存器 SI (Source Index):源变址寄存器;
- DI (Destination Index):目的变址寄存器;
- 控制寄存器 IP (Instruction Pointer):指令指针寄存器; FLAG:标志寄存器;
- 段寄存器 CS (Code Segment):代码段寄存器; DS (Data Segment):数据段寄存器; SS (Stack Segment):堆栈段寄存器; ES (Extra Segment):附加段寄存器;