一步步写操作系统(三)
3.C语言和asm语言的相互调用
前面的boot和loader都准备好了,就差kernel了。
首先我们要明确,kernel用什么语言写。boot和loader使用asm语言编写,是因为系统的特性,它需要最基本的元素来操作磁盘、端口、内存。然而如果kernel使用asm编写,对于开发难度来说是一个很大的挑战。虽然人生要面临很多挑战,我们都因该从容接受,但是对于只要了解操作系统的我们来说这个挑战无疑是浪费时间的,因为我们本来就是在重复造轮子。我们的期望只是,通过重复造出一个简单的轮子,来更加了解并熟练运用已有的轮子。所以不希望使用asm而是用C语言来编写。至于为什么不用C++,我已经在帖子 使用C语言的struct来实现C++的class中说明了,在很不明确C++编译器而又要使用编译器结果的地方,还是不要贸然使用C++。但是一些系统的底层操作,比如端口操作,以及中断门、调用门等,还是离不开asm的。接下来我们看一下C语言和asm语言的相互调用的用法:
a.使用__asm__来实现C调用asm
__asm__("asm" : "=out" (outParam) : "in"(inParam));
比如将port端口通过in读取数据:
__asm__("in %%dx, %%al" : "=a" (result) : "d"(port));
b.更一般的方法:
asm中使用global引出函数名,编译成.o文件,和使用这个函数名的.c文件编译成的.o文件进行连接,就可以实现调用:
system_lib.asm
使用中断门时需要调用汇编的中断处理子程序,子程序又会有一个回调,在这样一个互相调用的模式中,一方需要先声明,而asm是不可能包含这个外部声明的,所以可以将asm的声明放置到.h中:
system_call.asm
system_call.h
这里应该就能看明白了,asm编译出来的变量、函数label和c语言编译出来的label如出一辙,因此可以调用。而C++编译出来的并不是这种形式的label,因此不能调用。
-----------------------------------------------------------------------------------------------------------------------------------------------------------
system_lib.asm
启动完成配置好bochs,运行会在屏幕上出现一个"Hello OS World"字样。
3.C语言和asm语言的相互调用
前面的boot和loader都准备好了,就差kernel了。
首先我们要明确,kernel用什么语言写。boot和loader使用asm语言编写,是因为系统的特性,它需要最基本的元素来操作磁盘、端口、内存。然而如果kernel使用asm编写,对于开发难度来说是一个很大的挑战。虽然人生要面临很多挑战,我们都因该从容接受,但是对于只要了解操作系统的我们来说这个挑战无疑是浪费时间的,因为我们本来就是在重复造轮子。我们的期望只是,通过重复造出一个简单的轮子,来更加了解并熟练运用已有的轮子。所以不希望使用asm而是用C语言来编写。至于为什么不用C++,我已经在帖子 使用C语言的struct来实现C++的class中说明了,在很不明确C++编译器而又要使用编译器结果的地方,还是不要贸然使用C++。但是一些系统的底层操作,比如端口操作,以及中断门、调用门等,还是离不开asm的。接下来我们看一下C语言和asm语言的相互调用的用法:
a.使用__asm__来实现C调用asm
__asm__("asm" : "=out" (outParam) : "in"(inParam));
比如将port端口通过in读取数据:
__asm__("in %%dx, %%al" : "=a" (result) : "d"(port));
b.更一般的方法:
asm中使用global引出函数名,编译成.o文件,和使用这个函数名的.c文件编译成的.o文件进行连接,就可以实现调用:
system_lib.asm
[bits 32]
global disp_pos
global disp_str_asm
[section .data]
disp_pos dd 0xb8000
[section .text]
disp_str_asm:
.....
lib.asm
extern unsigned int disp_pos;
void disp_str(char* str){
disp_str_asm(str);
}
这样的代码,在编译器链接阶段会自动找到disp_str_asm和disp_pos标签。
c. C语言和asm语言的相互调用使用中断门时需要调用汇编的中断处理子程序,子程序又会有一个回调,在这样一个互相调用的模式中,一方需要先声明,而asm是不可能包含这个外部声明的,所以可以将asm的声明放置到.h中:
system_call.asm
[bit 32]
global int_handler21_asm
[section .text]
[extern int_handler21]
; void int_handler21_asm()
int_handler21_asm:
push ebp
mov ebp, esp
call int_handler21
leave
iretd
system_call.h
extern void int_handler21_asm();
key.c
#include "../kernel/system_call.h"
void int_handler21() {
.....
}
interrupt.h
#include "../kernel/system_call.h"
void init_gdtidt() {
.......
// key IRQ 0x21
set_gatedesc(idt + 0x21, (DWORD)int_handler21_asm, 1 * 8, 0x008e);
.......
}
这里应该就能看明白了,asm编译出来的变量、函数label和c语言编译出来的label如出一辙,因此可以调用。而C++编译出来的并不是这种形式的label,因此不能调用。
-----------------------------------------------------------------------------------------------------------------------------------------------------------
使用上面b的例子,可以写一个简单的内核了
我们使用第一帖使用一步步写操作系统(二) Boot启动或者经过扩展的一步步写操作系统(二) Boot启动 解决boot过大的问题的代码进行引导,这里只给出kernel的代码。
我们将引导代码放到boot/文件夹中,kernel放到kernel/文件夹中,lib放到lib/文件夹中。
#include "../lib/lib.h"
int main() {
disp_pos = 0xb8000;
disp_str("Hello OS World");
}
system_lib.asm
[bits 32]
; system_lib.asm
; author stophin
;
; System lib functions, will be invoked by C kernel
[bits 32]
global disp_pos, disp_base
global disp_str_asm
[section .data]
disp_base dd 0xb8000
disp_pos dd 0xb8000
[section .text]
; void disp_str_col_asm(char *)
disp_str_asm:
push ebp
mov ebp, esp
mov esi, [ebp + 8]
mov edi, [disp_pos]
mov ah, 0Fh
.1:
lodsb
test al, al
jz .2
cmp al, 0Ah ; return?
jnz .3
push eax
mov eax, edi
sub eax, [disp_base]
mov bl, 160 ; 80 * 2
div bl
and eax, 0FFh
inc eax
mov bl, 160
mul bl
add eax, [disp_base]
mov edi, eax
pop eax
jmp .1
.3:
mov [edi], ax
add edi, 2
jmp .1
.2:
mov [disp_pos], edi
mov esp, ebp
pop ebp
ret
lib.c
extern unsigned int disp_pos;
void disp_str(char* str){
disp_str_asm(str);
}
然后是最重要的一步,因为我们在连接所有.o文件时,不论是.asm生成的还是.c生成的都会按照ld参数顺序连接起来。我们希望从kernel的main开始,
但是有时候kernel也需要调用一些库,这些库需要连接在kernel前面否则找不到定义。那么我们可以使用一个特定的库,使得它被连接在kernel.bin的最前面,
然后让它调用kernel的main就可以了。这个库就是kernel_entry.asm
; kernel_entry.asm
; author stophin
;
; ensure that we jump straight into the kernel's entry function
[bits 32] ; we're in protected mode by now, so use 32-bit instructions
[extern main] ; declate that we will be referencing the external symbol main
; so the linke can substitute the final address
mov byte [0xb8000], 102
call main ; invoke main() in our c kernel
jmp $ ; hang forever when we return from the kernel
首先打印一个'f'字符说明进入entry,然后调用唯一的kernel的main函数,进入kernel。
然后修改Makefile来制作操作系统:
# Automatically generate lists of sources using wildcard
SOURCES = $(wildcard kernel/*.c lib/*.c )
SYSTEMC = $(wildcard kernel/*.asm)
HEADERS = $(wildcard kernel/*.h lib/*.h)
# Convert the *.c filenames to *.o to give a list of object files to build
# please notice the order of the object
# you can add external object which created by other tools
ENREY = kernel/kernel_entry.o
EXTERNAL =
OBJECT = $(SYSTEMC:.asm=.o) ${SOURCES:.c=.o}
# Output image directory
IMAGE_DIR = ./
# OS name
IMAGE = ${IMAGE_DIR}nano
# Default build target
all: image
# This is the actual disk image that the computer loads
# which is the combination of our compiled bootsector and kernel
image : boot/boot.bin boot/loader.bin ${IMAGE_DIR}kernel.bin
cp $< ${IMAGE_DIR}
cat $^ > ${IMAGE}.bin
dd if=${IMAGE}.bin of=${IMAGE}.img bs=1440K count=1 conv=notrunc
# This builds the binary of our kernel from two object files:
# - the kernel entry, which jumps into main() in our kernel
# - the compiled C kernel
${IMAGE_DIR}kernel.bin : ${ENREY} ${EXTERNAL} ${OBJECT}
ld -o ${IMAGE_DIR}kernel.elf -s -Ttext 0x9000 -e main -m elf_i386 $^
objcopy -I elf32-i386 -O binary -R .note -R .comment -S ${IMAGE_DIR}kernel.elf $@
# Generic rule for compiling C code to an object file
# For simplicity, we C files depend on all header files
%.o : %.c ${HEADERS}
gcc -O0 -ffreestanding -m32 -c $< -o $@
# Assemble the kernel entry
%.o : %.asm
nasm $< -f elf32 -o $@
# Assemble binary
%.bin : %.asm
nasm $< -f bin -o $@ -I boot/
# Clean
clean:
rm -rf ${IMAGE_DIR}*.bin ${IMAGE_DIR}*.elf boot/boot.bin boot/loader.bin
rm -rf ${ENTRY} ${OBJECT}
# White image
raw :
dd if=/dev/zero of=${IMAGE}.img bs=1440K count=1
如上Makefile使用wildcard来自动生成编译对象,make一下应该就可以了,使用make clean和make raw初始化。(注意Makefile期望每个命令前面是Tab但本站Tab被转码为空格,所以要将空格转换为TAB再编译)不过这里使用的是-tText 0x9000,因此请使用
一步步写操作系统(二) Boot启动或者经过扩展的
一步步写操作系统(二) Boot启动 解决boot过大的问题的代码进行启动。
启动完成配置好bochs,运行会在屏幕上出现一个"Hello OS World"字样。