创建main函数和链接C文件
??一般C语言使用main()函数作为程序的入口点, 为了符合我们平时的编程习惯, 这里我们也使用main()函数作为C代码的入口点, 并在"start.asm"文件中添加中断服务程序来调用C函数。
??在这一节教程,我们将尝试创建一个"main.c"文件和一个包含常用函数原型的头文件"system.h"。"main.c"中包含mian()函数, 它将作为你C代码的入口。在内核开发中, 我们一般不从main()函数返回。多数操作系统在main中初始化内核和子程序、加载shell, 然后main函数会进入空循环中。在多任务系统中, 当没有其他需要运行的任务时, 将一直执行这个空循环。下面是"main.c"文件的示例,其中包含了最基本的main()函数和一些我们以后会用到的函数体。
main.c
#include <system.h> /* 你将要自己完成这些代码 */ unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count) { /* 在此处添加代码, 将'src'中count字节的数据复制到'dest'中, * 最后返回'dest' */ } unsigned char *memset(unsigned char *dest, unsigned char val, int count) { /* 在此处添加代码, 将'dest'中的count字节全部设置成值'val', * 最后返回'dest' */ } unsigned short *memsetw(unsigned short *dest, unsigned short val, int count) { /* 在此处添加代码, 将'dest'中的count双字节设置成值'val', * 最后返回'dest' * 注意'val'是双字节 16-bit*/ } int strlen(const char *str) { /* 返回字符串的长度 * 遇到某个字节的值为0x00结束 */ } /* 我们之后将使用这个函数通过IO端口从设备读取数据, 如键盘等 * 我们使用内联汇编代码(inline assembly)实现该功能 */ unsigned char inportb (unsigned short _port) { unsigned char rv; __asm__ __volatile__ ("inb %1, %0" : "=a" (rv) : "dN" (_port)); return rv; } /* 我们将使用这个函数通过I/O端口向设备写数据 * 用来修改文本模式和光标位置 * 同样,我们使用内联汇编来实现这个单用C无法实现的功能 */ void outportb (unsigned short _port, unsigned char _data) { __asm__ __volatile__ ("outb %1, %0" : : "dN" (_port), "a" (_data)); } /* 这是一个非常简单的main函数, 它内部仅进行死循环 */ void main() { /* 你可以在这里添加语句 */ /* 保留此循环 * 不过, 在'start.asm'里也有一个无限循环, 防止你不小心删除了下面这一行*/ for (;;); }
??在你编译之前, 我们需要在‘start.asm‘中添加2行代码。我们需要让编译器知道main()在外部文件中, 我们还需要从‘start.asm‘文件中调用main()函数。打开‘start.asm‘文件, 在stublet:
的下面添加下面2行代码:
extern _main call _main
??先等等编译, _main
前面的下划线是什么东西, 我们在C语言里声明的是main
啊?编译器gcc在编译时会在所有函数和变量名前面加上下划线。因此, 要从汇编中引用C文件中的函数和变量, 我们都需要在前面加上下划线!
??现在我们还缺少一个"system.h"文件。创建一个名为"include"的文件夹, 在该文件加下创建一个名为"system.h"的空白文本文件, 将mencpy
、memset
、memsetw
、strlen
、inportb
、outportb
这些函数的函数原型添加到该头文件中。#ifndef
、#define
、#endif
用于防止头文件被多次声明。这个头文件用来包含你在内核中使用的所有函数, 你可以随意添加你需要的函数来扩展此库。
include/system.h
#ifndef __SYSTEM_H #define __SYSTEM_H /* MAIN.C */ extern unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count); extern unsigned char *memset(unsigned char *dest, unsigned char val, int count); extern unsigned short *memsetw(unsigned short *dest, unsigned short val, int count); extern int strlen(const char *str); extern unsigned char inportb (unsigned short _port); extern void outportb (unsigned short _port, unsigned char _data); #endif
??接下来, 我们要来编译这些文件。打开之前的"build.bat"文件, 添加下面一行命令来编译你的"main.c"。这个命令运行了gcc编译器, gcc
后面有一堆参数:
-Wall
用于显示所有的警告-O
、-fstrength-reduce
、-fomit-frame-pointer
、-finline-functions
用于编译优化-nostdinc
和-fno-builtin
告诉编译器我们将不适用C标准库函数-I./include
告诉编译器我们的头文件在当前文件夹下的"include"文件夹里-c
表示当前仅编译, 不链接-o main.o
表示输出文件名为main.omain.c
是我们要编译的文件
gcc -Wall -O -fstrength-reduce -fomit-frame-pointer -finline-functions -nostdinc -fno-builtin -I./include -c -o main.o main.c
??别忘了, 按照"build.bat"文件中的指示, 把"main.o"添加到链接文件的列表中去。像这样:
ld -T link.ld -o kernel.bin start.o main.o
??当前完整的"build.bat"文件内容如下:
build.bat
echo Now assembling, compiling, and linking your kernel: nasm -f aout -o start.o start.asm rem Remember this spot here: We will add 'gcc' commands here to compile C sources gcc -Wall -O -fstrength-reduce -fomit-frame-pointer -finline-functions -nostdinc -fno-builtin -I./include -c -o main.o main.c rem This links all your files. Remember that as you add *.o files, you need to rem add them after start.o. If you don't add them at all, they won't be in your kernel! ld -T link.ld -o kernel.bin start.o main.o echo Done! pause
??最后, 如果你不知道该怎么实现那些附加函数, 如memcpy
函数, 这里有一份参考代码:
#include <system.h> unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count) { const unsigned char *sp = (const unsigned char *)src; unsigned char *dp = dest; for(; count != 0; count--) *dp++ = *sp++; return dest; } unsigned char *memset(unsigned char *dest, unsigned char val, int count) { unsigned char *temp = (unsigned char *)dest; for( ; count != 0; count--) *temp++ = val; return dest; } unsigned short *memsetw(unsigned short *dest, unsigned short val, int count) { unsigned short *temp = (unsigned short *)dest; for( ; count != 0; count--) *temp++ = val; return dest; } int strlen(const char *str) { int retval; for(retval = 0; *str != '\0'; str++) retval++; return retval; } /* 我们之后将使用这个函数通过IO端口从设备读取数据, 如键盘等 * 我们使用内联汇编代码(inline assembly)实现该功能 */ unsigned char inportb (unsigned short _port) { unsigned char rv; __asm__ __volatile__ ("inb %1, %0" : "=a" (rv) : "dN" (_port)); return rv; } /* 我们将使用这个函数通过I/O端口向设备写数据 * 用来修改文本模式和光标位置 * 同样,我们使用内联汇编来实现这个单用C无法实现的功能 */ void outportb (unsigned short _port, unsigned char _data) { __asm__ __volatile__ ("outb %1, %0" : : "dN" (_port), "a" (_data)); } /* 这是一个非常简单的main函数, 它内部仅进行死循环 */ void main() { /* 你可以在这里添加语句 */ /* 保留此循环 * 不过, 在'start.asm'里也有一个无限循环, 防止你不小心删除了下面这一行*/ for (;;); }
PS: 下面是我自己写的
Win10安装gcc编译器
??Win10安装gcc、g++、make: https://www.cnblogs.com/raina/p/10656106.html
本节教程对应的Linux下的编译脚本
echo "Now assembling, compiling, and linking your kernel:" nasm -f elf64 -o start.o start.asm # Remember this spot here: We will add 'gcc' commands here to compile C sources gcc -Wall -O -fstrength-reduce -fomit-frame-pointer -finline-functions -nostdinc -fno-builtin -I./include -c -o main.o main.c # This links all your files. Remember that as you add *.o files, you need to # add them after start.o. If you don't add them at all, they won't be in your kernel! ld -T link.ld -o kernel.bin start.o main.o echo "Done!" read -p "Press a key to continue..."
_main的问题
??我的gcc版本是7.4.0, 在编译C程序时并没有在函数和变量名前面自己加下划线, 所以按照原教程写的汇编代码, 编译后会报下面的错误:
start.o: In function `stublet‘:
start.asm:(.text+0x29): undefined reference to `_main‘
把"start.asm"里_main
的下划线去掉, 再编译就行了。