实验目的:
1、加深理解操作系统内核概念
2、了解操作系统开发方法
3、掌握汇编语言与高级语言混合编程的方法
4、掌握独立内核的设计与加载方法
5、加强磁盘空间管理工作
实验要求:
1、知道独立内核设计的需求
2、掌握一种x86汇编语言与一种C高级语言混合编程的规定和要求
3、设计一个程序,以汇编程序为主入口模块,调用一个C语言编写的函数处理汇编模块定义的数据,然后再由汇编模块完成屏幕输出数据,将程序生成COM格式程序,在DOS或虚拟环境运行。
4、汇编语言与高级语言混合编程的方法,重写和扩展实验二的的监控程序,从引导程序分离独立,生成一个COM格式程序的独立内核。
5、再设计新的引导程序,实现独立内核的加载引导,确保内核功能不比实验二的监控程序弱,展示原有功能或加强功能可以工作。
6、编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性
实验内容:
(1) 寻找或认识一套匹配的汇编与c编译器组合。利用c编译器,将一个样板C程序进行编译,获得符号列表文档,分析全局变量、局部变量、变量初始化、函数调用、参数传递情况,确定一种匹配的汇编语言工具,在实验报告中描述这些工作。
(2)写一个汇编程和c程序混合编程实例,展示你所用的这套组合环境的使用。汇编模块中定义一个字符串,调用C语言的函数,统计其中某个字符出现的次数(函数返回),汇编模块显示统计结果。执行程序可以在DOS中运行。
(3) 重写实验二程序,实验二的的监控程序从引导程序分离独立,生成一个COM格式程序的独立内核,在1.44MB软盘映像中,保存到特定的几个扇区。利用汇编程和c程序混合编程监控程序命令保留原有程序功能,如可以按操作选择,执行一个或几个用户程序、加载用户程序和返回监控程序;执行完一个用户程序后,可以执行下一个。
(4) 利用汇编程和c程序混合编程的优势,多用c语言扩展监控程序命令处理能力。
(5) 重写引导程序,加载COM格式程序的独立内核。
(6)拓展自己的软件项目管理目录,管理实验项目相关文档
我们知道C和nasm到最后都可以变成一堆机器码, 然后被CPU执行. 那么用C语言的函数设计方法和数据结构帮助我们开发操作系统就比单纯用汇编手撸方便很多. 在两者中实现交互的核心是变量和函数的共享.
在编写asm汇编文件时, 因为需要从C语言的文件中使用函数, 我们就要一行代码告诉编译器我要从其他模块中找到这个函数, 同样, 变量也要同样这样做
extern FuncFromC
extern VariableFromC
当然, C语言中也会想要调用汇编中的函数, 这时就用global修饰入口名, 导出这个入口, 让链接器识别并让C语言调用.
global FuncFromASM
global VariableFromASM
如果要用C调用asm中的函数, 那这个FuncFromASM当然也要遵循C调用约定, 后面的参数先入栈, 并由调用者清理堆栈. 为了理解这个过程, 我们可以观察gcc编译一个程序后做了什么事情. 比如这里我们写一个比较两个整形的程序.
compare.c
int num1st = 3;
int num2nd = 4;
int flag = 0;
void cmp(int a, int b) {
if (a > b) flag = 1;
}
void main(void)
{
cmp(num1st,num2nd);
}
cmd里敲 gcc -S compare.c就能在当前路径下得到汇编码文件compare.s. 然后我们看一看几段比较关键的汇编码
compare.s(部分)
_cmp:
LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
movl 8(%ebp), %eax
cmpl 12(%ebp), %eax
jle L3
movl $1, _flag
L3:
nop
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
_main:
LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
call ___main
movl _num2nd, %edx
movl _num1st, %eax
movl %edx, 4(%esp)
movl %eax, (%esp)
call _cmp
nop
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
我们只关心最重要的函数参数压栈等过程. 首先从main入口进入, 把num2nd放到4(%esp), num1st放到(%esp), 这就是所谓的后面的参数先入栈. 然后我们调用cmp, 这时函数的返回地址入栈, 然后为了保护%ebp, ebp入栈, 这样栈里多了两个东西, 8个字节. 原来的num2nd和num1st在栈中的位置变成了4+8(%ebp)= 12(%ebp)和0+8(%ebp)= 8(%ebp). 接下来的代码, 用cmp和jle实现比较if语句赋值. 当num2nd小于等于num1st时就什么都不做, 否则赋值flag为1. 到此为止, 我们的C代码的工作就做完了.
看了这段代码, 我们就知道在编写自己的汇编程序时, 要怎么使用C文件里的函数了. 我们只需要模仿上面的代码进行压栈, 然后call目标函数, 就能实现函数传参和调用. 如果觉得函数调用麻烦, 也可以直接让C的函数操作asm中的变量, 不过那样显然不够优雅.
看了那么多, 是不是应该自己试一试效果呢? 实验内容(2)要求我们实现在汇编中定义字符串, 在C中实现字符串的字符统计, 然后回到汇编打印统计结果.
那么就先写一个能统计字符串的C程序好了, 相信都大二了, 写这种程序应该秒implement. 以求简便, 我们只统计字符串中a字符的出现次数.
counter.c
__asm__(".code16gcc\n");
char CountStr[10] = {0,0,0,0,0,0,0,0,0,0};
void counta(char* str_in, int len) {
char str[10] = "aaabbbcccd";
int counter = 0;
for (int i = 0;i < len;i++) {
if (str_in[i]=='a') counter += 1;
}
int counter_str_len = 0;
int tmp = counter;
while (tmp != 0) {
tmp /= 10;
counter_str_len++;
}
for (int i = counter_str_len - 1;i >= 0;i--) {
CountStr[i] = (counter % 10)+'0';
counter = counter/10;
}
}
char in[10] = "abcdaaccdd";
int len = 10;
void main(void)
{
counta(in, len);
}
我们在nasm编程里照葫芦画瓢把参数push到栈里即可.顺序是逆序的. 并且我们看见, 函数返回时要用一个add把栈指针增加8字节, 把压栈的2个参数收回. 下面再用nasm写一下汇编中的字符串定义, 函数调用和统计值打印.
需要注意的细节有, nasm环境是纯粹的16位, 不是c里的假的16位, 但是我们传到c函数里的地址是32位, 因此传char*的指针地址时要按小端序, 先push cs再push偏移.
另外, 我们在nasm中希望使用c里定义的函数counta和变量CountStr, 这两个在前面加extern修饰. 另外用global修饰入口_start, 让编译器识别.如果发现链接后的二进制文件很混乱, 数据和代码混在一起了, 就用[section .data]和[section .text]规定数据和代码都放在什么地方.
printer.asm
BITS 16
extern counta
extern CountStr
global _start
;global CountStr
[section .text]
_start:
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
_call:
mov bp, Message
mov cx, 10
mov ax, 1301h
mov bx, 0007h
mov dh, 5
mov dl, 5
int 10h
push dword[msglen]
mov ax, Message
add word[loc32], ax
push dword[loc32]
push cs
call counta
add esp, 8
; 上面调用counta函数统计Message的a字符数目
; 以字符串形式把统计值保存在CountStr中
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov bp, CountStr
mov cx, 10
mov ax, 1301h
mov bx, 0007h
mov dh, 13
mov dl, 13
int 10h
; 这段是定位段和bios调用打印字符串
_end:
jmp $
[section .data]
Message db "abcaadacaa"
loc32 dd 0
msglen dd 10
我们把这两个文件编译并链接, 可以得到可执行文件. makefile的批处理命令如下
nasm -f elf32 2printer.asm -o 2printer.o
gcc -c 2counter.c -o 2counter.o
ld.exe -melf_i386 -N 2printer.o 2counter.o -Ttext 0x7c00 --oformat binary -o boot.bin
objdump -D -b binary -m i386 boot.bin > boot.i
gcc -Og -S 2counter.c -o 2counter.s
除了编译和链接, 我们还objdump出来指令文件和汇编码方便debug. 用winhex打开发现没到512字节, 那么好, 我们把它做成引导扇区直接在虚拟机上跑起来.
统计成功, a在字符串中出现了6次.
到这里, 我们已经完成了第一个gcc和nasm混合编程的程序. 使用C开发操作系统的时代到来了, 后面我们会使用相同的技术, 把一些更有挑战性的工作交给C处理, 就像上面的工作一样, 即使只是一个itoa这样的接口, 如果用nasm自己实现, 我想没有半小时是搞不定的吧, 但是用C只需要几分钟.
我们在实验2里开发了一个监控程序, 它具备基本的输入输出功能, 基本的读扇区能力写内存和跳转能力. 很显然, 读扇区写内存和跳转都不是C擅长的, 因此我们仍然用汇编实现这些功能, 但是屏幕的输入输出, 串处理和循环等功能, C会比汇编做得更好.
上一个实验, 我们的程序接收用户输入1~4来启动用户程序, 通过自定义中断终止程序运行. 现在, 我们可以用C把这件事情做得更好, 我们在C中定义表, 并把程序名和程序在扇区中的实地址记录在表里, 这样就能用类似shell的命令进行输入输出了. 另外, 我们还可以定义任务栈实现批处理. 为此, 我们需要在c中定义相当多的辅助函数和变量, 如字符串比较的strcmp, 打印字符串的print 接收输入的input, 管理用户程序的结构体和表, 任务栈和入栈出栈函数等等. 这些工作我们将分步实现.
(*
首先简单总结一下前面的实验告诉我们的知识, 所有汇编与c相互调用需要做的事情
void putch(char ch, int offset);
void cls();
void clear(){
cls();
X = 0;Y = 0;
}
void print(char* msg){
int offset;
for (;*msg;msg++){
offset = (X*80+Y)*2;
if (*msg != '\r' && *msg != '\n'){
putch(*msg, offset);
Y++;
}
else{
X++;
Y = 0;
}
if (Y==SCREEN_WIDTH){
Y = 0; X++;
if (X==SCREEN_HEIGHT)
X = 0;
}
}
}
void input(char* s){
char tmp[2] = " \0";
for (;; s++)
{
*s = _getch();
*tmp = *s;
print(tmp);
if (*s == '\r' || *s == '\n')
break;
}
*s = '\0';
}
char _getch(){
char ch;
asm volatile("int $0x16\n"
: "=a"(ch)
: "a"(0x0000));
return ch;
}
int _strcmp(char* str1, char* str2){
for (;;str1++,str2++){
if (!*str1){
if (!*str2) return 0;
else return 1;
}
else if (*str1 && !*str2) return -1;
if (*str1<*str2) return 1;
else if(*str1>*str2) return -1;
}
return 0;
}
int _strlen(char* s){
int len = 0;
for (;*s;len++){}
return len;
}
cls:
push es
push si
mov ax, 0B800h
mov es, ax
mov si, 0
mov cx, 80*25 ; 循环次数
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
pop si
pop es
retf
putch:
push es
push si
mov ax, 0B800h
mov es, ax
mov ax, word[esp + 12]
mov si, ax
mov dl, byte[esp + 8]
mov dh, 0Fh
mov [es:si], dx
pop si
pop es
retf
引导扇区和内核分离
我们之前的程序都是逻辑简单而且比较短的程序, 可以直接放进512字节的首扇区里, 但是当我们使用c和汇编共同作业时, 生成的代码量将会比之前多很多. 显然512字节已经不太足够我们使用了, 为此我们要把我们写好的主程序(C+nasm)封装成COM程序, 用引导扇区把这段程序加载到内存并执行. 加载用户程序的方法上一个实验我们实现过. 我们在引导扇区通过bios调用加载代码和数据进内存, 再用jmp跳转到目标位置, 从此机器就完全由目标位置的主程序接管.
下面给出一个简单的密码操作系统内核, 在开机后它会反复请求用户输入五位的有效密码, 直到密码正确, 机器退出循环并打印问候语welcome.
kernel.asm
BITS 16
extern main
extern info
extern print
extern password
%macro print 4 ; string, length, x, y
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov bp, %1
mov cx, %2
mov ax, 1301h
mov bx, 000fh
mov edx,0
mov dh, %3
mov dl, %4
int 10h
%endmacro
[section .text]
global _start
global cls
global putch
;global getch
targetAddr dw 0
%macro _CALL 2
push eax
mov ax, %1
mov [targetAddr+2], ax ;targetaddr为0
mov ax, %2
mov [targetAddr], ax
pop eax
call far [targetAddr]
%endmacro
_start:
_CALL cs, main
print info,7,10,10
jmp $
ckernel.c(main)
void main(void){
while(1){
print("Enter the password: \0");
input(user_str);
if (_strcmp(user_str,password)==0)
break;
else
print("incorrect password.\0");
_getch();
clear();
}
print("correct password.\0");
}
leader.asm
bits 16
org 7c00h
OffSetOfKerPrg equ 0A100h
mov cl, 2 ; 从第二个扇区开始
mov ax, cs ; 定位
mov ds, ax ; DS = CS
mov es, ax ; ES = CS
mov ss, ax ; SS = CS
mov ah, 2 ;读扇区
mov al, 3 ;读扇区数
mov dl, 0 ;驱动器号
mov dh, 0 ;磁头号
mov ch, 0 ;柱面号
mov bx, OffSetOfKerPrg ;数据缓冲区
int 13H
jmp OffSetOfKerPrg ;跳转到内核
times 510-($-$$) db 0
dw 0xaa55
我们把leader编译得到的二进制引导扇区文件boot.bin放在第一个扇区, 内核编译得到的COM程序文件放在第二个和第三个扇区, 执行可以看到下面的结果
程序的逻辑很简单, 这个程序主要是为了验证我们的C与汇编是否正常协同工作. 这样看来应该是问题不大了, 下面我们真正来开发一个简陋的shell.
C Shell
我们这个简单的shell要实现的功能是, 允许主程序执行用户程序并监控返回. 首先我们把之前在实验2中实现的加载用户程序的汇编代码封装成能在C中调用的函数. 它接受一个扇区号, 然后进行bios调用加载程序并跳转.
除此之外, shell中设置记录用户程序位置的表和用户程序栈, 程序将按顺序出栈并依次执行.
cshell.c
__asm__(".code16gcc");
#define PRG_NUM 4
#define SCREEN_WIDTH 80
#define SCREEN_HEIGHT 25
void putch(char ch, int offset); //from asm
void cls(); //from asm
void LoadProgram(int, int); //from asm
void CallProgram(); //from asm
void load_user_prg(int, int); //加载执行用户程序
void clear(); //清屏
void print(char* ); //打印字符串
void input(char* ); //键盘读入字符串
char _getch(); //键盘读入单个字符
int _strcmp(char* , char* ); //比较字符串
int _strlen(char* ); //获取字符串长度
char info[12] = "system halt\0";
int TaskStack[100];
char cmd[100];
int X,Y; //光标
int stack_length;
char intchar[5] = "0\0";
typedef struct Prg{
char name[5];
int size;
int address;
}Program;
//定义用户程序表, 结构体中存储程序名, 字节数, 扇区数
Program PrgTable[PRG_NUM] = {
{"lu\0",255,11},
{"ru\0",255,12},
{"ll\0",255,13},
{"rl\0",255,14},
};
void clear(){
cls();
X = 0;Y = 0;
}
void print(char* msg){
int offset;
for (;*msg;msg++){
offset = (X*80+Y)*2;
if (*msg != '\r' && *msg != '\n'){
putch(*msg, offset);
Y++;
}
else{
X++;
Y = 0;
}
if (Y>=SCREEN_WIDTH){
Y = 0; X++;}
if (X>=SCREEN_HEIGHT)
X = 0;
}
}
void input(char* s){
char tmp[2] = " \0";
for (;; s++)
{
*s = _getch();
*tmp = *s;
print(tmp);
if (*s == '\r' || *s == '\n')
break;
}
*s = '\0';
}
char _getch(){
char ch;
asm volatile("int $0x16\n"
: "=a"(ch)
: "a"(0x0000));
return ch;
}
int _strcmp(char* str1, char* str2){
for (;;str1++,str2++){
if (!*str1){
if (!*str2) return 0;
else return 1;
}
else if (*str1 && !*str2) return -1;
if (*str1<*str2) return 1;
else if(*str1>*str2) return -1;
}
return 0;
}
int _strlen(char* s){
int len = 0;
for (;*s;len++){}
return len;
}
void run_user_prg(int sec, int file_size) {
int snum = file_size/512+1;
LoadProgram(sec,snum);
print("press any key to start\n\0");
_getch();
clear();
CallProgram();
clear();
}
void printint(int x){
intchar[0] = '0'+x;
print(intchar);
}
void main(void)
{
X = 0; Y = 0;
stack_length = 0;
//run_user_prg(PrgTable[1].address, PrgTable[1].size);
print("Type help for command list.\n\0");
while (1) {
print(">>\0");
input(cmd);
if (!_strcmp(cmd, "help")) {
print("shell\n\0");
print("you can enter following command for execute a user\'s program\n\0");
print("ls: list all the user\'s programs\' information\n\0");
print("[prg name]: enter any program\'s name to push it into task stack, it will be executed soon\n\0");
print("exec: execute all the program in the stack\n\0");
print("clear: clear your screen\n\0");
print("exit: exit your os\n\0");
}
else if (!_strcmp(cmd, "ls")) {
for (int i = 0;i < PRG_NUM;i++) {
print("Program \0");
printint(i+1);
print(": \0");
print(PrgTable[i].name);
print("\n\0");
}
}
else if (!_strcmp(cmd, "clear"))
clear();
else if (!_strcmp(cmd, "exec")) {
while (stack_length>0) {
int indice = TaskStack[stack_length-1];
print("Execute program \0");
print(PrgTable[indice].name);
print("\n\0");
run_user_prg(PrgTable[indice].address, PrgTable[indice].size);
stack_length--;
}
}
else if (!_strcmp(cmd, "exit")){
print("Goodbye!\0");
break;
}
else {
int found = 0;
for (int i = 0;i < PRG_NUM;i++) {
if (!_strcmp(cmd, PrgTable[i].name)) {
TaskStack[stack_length++] = i;
print("it is now in stack, stack length \0");
printint(stack_length);
print("\n\0");
found = 1;
break;
}
}
if (!found) {
print("invalid command, type help for command list.\n\0");
}
}
}
}
kernel.asm
BITS 16
extern main
extern info
global _start
global cls
global putch
global LoadProgram
global CallProgram
OffSetOfUserPrg equ 0100h
%macro print 4 ; string, length, x, y
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov bp, %1
mov cx, %2
mov ax, 1301h
mov bx, 000fh
mov edx,0
mov dh, %3
mov dl, %4
int 10h
%endmacro
%macro _CALL 2
push eax
mov ax, %1
mov [targetAddr+2], ax ;targetaddr为0
mov ax, %2
mov [targetAddr], ax
pop eax
call far [targetAddr]
%endmacro
[section .text]
_start:
push es
push si
mov ax, 0000h ; 中断向量表从0h开始
mov es, ax
mov ax, 20h ; 重定义20号中断
mov bx, 4 ; 每个中断向量是4字节的地址,所以20h乘以4
mul bx
mov si, ax ; 偏移
mov ax, int20h
mov [es:si], ax ; 把我们的int20的偏移地址作为20号中断的中断服务程序
add si, 2
mov ax, cs ; 放入代码段, 符合中断向量的格式(前两字节为偏移,后两字节为代码段,且为大端序)
mov [es:si], ax
pop si
pop es
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
_CALL cs, main
print info,11,10,10
jmp $
;void LoadProgram(int sec, int snum)
LoadProgram:
mov ah, 2 ;读扇区
mov cl, byte[esp+4] ;读扇区号
mov al, byte[esp+8] ;读扇区数
mov dl, 0 ;驱动器号
mov dh, 0 ;磁头号
mov ch, 0 ;柱面号
mov bx, OffSetOfUserPrg ;数据缓冲区
int 13H
retf
;void CallProgram()
CallProgram:
_CALL cs, OffSetOfUserPrg
retf;
; void putc(char ch, int offset)
putch:
push es
push si
mov ax, 0B800h
mov es, ax
mov ax, word[esp + 12]
mov si, ax
mov dl, byte[esp + 8]
mov dh, 0Fh
mov [es:si], dx
pop si
pop es
retf
cls:
push es
push si
mov ax, 0B800h
mov es, ax
mov si, 0
mov cx, 80*25 ; 循环次数
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
pop si
pop es
retf
; 自定义的中断号
int20h:
mov ah, 01h ;缓冲区检测
int 16h
jz noclick ;缓冲区无按键
mov ah, 00h
int 16h
cmp ax, 2c1ah ; 检测Ctrl + Z
jne noclick
mov ax, 0fffh ;只是作为一个标志
noclick:
iret
[section .data]
targetAddr dw 0
bootloader.asm
bits 16
org 7c00h
OffSetOfKerPrg equ 0A100h
mov ax, cs ;定位es
mov es, ax
mov cl, 2 ;从第二个扇区开始
mov ah, 2 ;读扇区
mov al, 6 ;读扇区数
mov dl, 0 ;驱动器号
mov dh, 0 ;磁头号
mov ch, 0 ;柱面号
mov bx, OffSetOfKerPrg ;数据缓冲区
int 13H
jmp OffSetOfKerPrg ;跳转到内核
times 510-($-$$) db 0
dw 0xaa55
user.asm
BITS 16
org 00100h
mov ax, cs
mov ds, ax
mov es, ax
mov al, byte[upper]
add al, 5
mov byte[x],al
mov al, byte[left]
add al, 5
mov byte[y],al
loop1:
dec word[count] ; 递减计数变量
jnz loop1 ; >0:跳转;
mov word[count],delay
dec word[dcount] ; 递减计数变量
jnz loop1
mov word[count],delay
mov word[dcount],ddelay
; 以上是用一个二重循环实现时延50000*580个单位时间
jmp Entrance ;进行一个周期的工作
jmp $ ;halt
Entrance:
int 20h
cmp ax, 0fffh
jne NotRet
retf
NotRet:
jmp BoundaryCheckx
DispStr:
call Clear
call Reset
mov ax, Message ;打印字符串
mov bp, ax
mov cl, byte[Strlen] ;字符串长
mov ch, 0
mov ax, 01301h ;写模式
mov bx, 000fh ;页号0,黑底白字
mov dh, byte[x] ;行=x
mov dl, byte[y] ;列=y
int 10h ;10h号接口
Updatexy:
mov al, byte[x]
add al, byte[vx]
mov byte[x], al
mov al, byte[y]
add al, byte[vy]
mov byte[y], al
jmp loop1 ;无限循环
BoundaryCheckx:
mov al, byte[x]
add al, byte[vx] ;预测下一刻的x
cmp al, byte[upper] ;如果x小于上边界
jl Changevx ;更新vx
cmp al, byte[lower] ;如果x大于下边界
jg Changevx ;更新vx
BoundaryChecky:
mov al, byte[y]
add al, byte[vy]
cmp al, byte[left] ;如果y小于左边界
jl Changevy ;更新vy
add al, byte[Strlen];预测下一刻的yr=y+字符串长
cmp al, byte[right] ;如果yr大于下边界
jg Changevy ;更新vy
jmp DispStr ;如果不需要更新vx vy就继续打印流程
Changevx:
neg byte[vx]
jmp BoundaryChecky
Changevy:
neg byte[vy]
jmp DispStr
Clear:
mov ax, 0B800h
mov es, ax
mov si, 160
mov cx, 80*24 ; 循环次数
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
ret
;通过直接修改显存实现的清屏函数
Reset:
mov ax, cs
mov ds, ax
mov ax, ds
mov es, ax
ret
;把cs ds和es指向相同的内存
Message: db "sysu"
Strlen db $-Message
delay equ 50000
ddelay equ 2000
count dw delay
dcount dw ddelay
clearcount db 0
vx db 1
vy db 1
left db 0
upper db 13
right db 39
lower db 24
x db 0
y db 0
times 510-($-$$) db 0
dw 0xaa55
makefile.bat
nasm -f elf32 kernel.asm -o kernel.o
gcc -Og -c cshell.c -o cshell.o
ld.exe -melf_i386 -N kernel.o cshell.o -Ttext 0x0A100 --oformat binary -o kernel.com
nasm leader.asm -o boot.bin
nasm user.asm -o user.com
编译执行, 我们将能够得到com形式二进制文件user.com, kernel.com, 引导扇区程序boot.bin. 我们把boot.bin放入引导扇区, 把user.com以及和它类似的用户程序放在11 12 13 14四个扇区, 把kernel.com放在第二扇区之后的6个扇区内, 就可以用虚拟机执行我们自己的shell了.
可以实现命令行级别的交互工作和程序运行, 到此为止, 基本的内核功能就算开发完毕.
本实验还算是蛮消磨心智的, 最先遇到的问题是能否找到一条能顺利让c和汇编协同编译的一套软件, 以及合适的命令. 这里选择了开发比较简单的gcc和nasm, 我个人是感觉比老师推荐的tcc+tasm好很多. 然后我在正式实验前, 尝试了很多更简单的c+asm小程序的编写, 从而把所有可能遇到的坑都踩一遍, 比如不使用org要怎样告诉编译器内存偏移, c的32位地址和asm的16位近地址调用该如何协同统一, 段寄存器的修改会在怎样的程度上影响程序的运行, 如何保护寄存器等等. 幸运的是经过两天半的努力还是把这件事做完了, 虽然踩坑的过程比较痛苦, 但是真正把一整套流程做下来, 对我的提升还是相当大的,