TCC
本文描述了如何使用FASM和Tiny C Compiler(又名TCC)制作一个示例ELF内核。 也可以使用NASM(Bare_Bones_With_NASM)。 TCC是一个小型且快速的C编译器,它产生x86,x86_64或ARM代码,并生成PE或ELF可执行文件。 TCC正朝着完全符合ISOC99的方向发展,并且可以像FASM一样自我编译。(译者注:自我编译,指用自身的源代码和编译器可以编译出一个新的编译器)
TCC还包括一个链接器和一个汇编器(仅x86)。 但是此汇编器是有限的: 不支持16/64位,支持到MMX的指令。
注: Windows版本的TCC不生成ELF可执行文件,而只生成目标文件。 如果要在Windows上使用本教程,则需要在没有PE支持的情况下重新编译TCC。 如果不使用Windows,可以跳过此步骤。
构建TCC带ELF支持
Windows
1. 下载 TCC源代码和32位TCC和(如果你有64位操作系统) 64位TCC。
2. 解压源代码文件夹tcc-0.9.26。
3. 将32位TCC文件保存到TCC-0.9.26所在位置的名为“Win32”的文件夹中。 如果你有64位操作系统,请创建一个win64文件夹,并将64位文件保存到同一位置的名为 “win64” 的文件夹中。
4. 打开记事本或其他文本编辑器,然后输入以下内容:
@echo off
set \p VERSION = < .VERSION
echo > config.h #define TCC_VERSION "%VERSION%"
set targetP=I386
set B=32
goto begin
:x86_64
set targetP=X86_64
set B=64
goto begin
:begin
set targetF=PE
set CC=..\win%B%\tcc.exe -O0 -s -fno-strict-aliasing
set P=%B%
:start
if %targetF%==ELF set P=%B%-elf
set target=-DTCC_TARGET_%targetF% -DTCC_TARGET_%targetP%
:tools
%CC% %target% win%P%\tools\tiny_impdef.c -o win%P%\tiny_impdef.exe
%CC% %target% win%P%\tools\tiny_libmaker.c -o win%P%\tiny_libmaker.exe
:libtcc
if not exist win%P%\libtcc\nul mkdir win%P%\libtcc
copy libtcc.h win%P%\libtcc\libtcc.h
%CC% %target% -shared -DLIBTCC_AS_DLL -DONE_SOURCE libtcc.c -o win%P%\libtcc.dll
win%P%\tiny_impdef win%P%\libtcc.dll -o win%P%\libtcc\libtcc.def
:tcc
%CC% %target% tcc.c -o win%P%\tcc.exe -ltcc -Lwin%P%\libtcc
:copy_std_includes
copy include\*.h win%P%\include
:libtcc1.a
win%B%\tcc %target% -c lib\libtcc1.c -o win%P%\libtcc1.o
win%B%\tcc %target% -c win%P%\lib\crt1.c -o win%P%\crt1.o
win%B%\tcc %target% -c win%P%\lib\wincrt1.c -o win%P%\wincrt1.o
win%B%\tcc %target% -c win%P%\lib\dllcrt1.c -o win%P%\dllcrt1.o
win%B%\tcc %target% -c win%P%\lib\dllmain.c -o win%P%\dllmain.o
win%B%\tcc %target% -c win%P%\lib\chkstk.S -o win%P%\chkstk.o
goto lib%B%
:lib32
win%B%\tcc %target% -c lib\alloca86.S -o win%P%\alloca86.o
win%B%\tcc %target% -c lib\alloca86-bt.S -o win%P%\alloca86-bt.o
win%B%\tcc %target% -c lib\bcheck.c -o win%P%\bcheck.o
win%P%\tiny_libmaker win%P%\lib\libtcc1.a win%P%\libtcc1.o win%P%\alloca86.o win%P%\alloca86-bt.o win%P%\crt1.o win%P%\wincrt1.o win%P%\dllcrt1.o win%P%\dllmain.o win%P%\chkstk.o win%P%\bcheck.o
@goto the_end
:lib64
win%P%\tcc %target% -c lib\alloca86_64.S -o win%P%\alloca86_64.o
win%P%\tiny_libmaker win%P%\lib\libtcc1.a win%P%\libtcc1.o win%P%\alloca86_64.o win%P%\crt1.o win%P%\wincrt1.o win%P%\dllcrt1.o win%P%\dllmain.o win%P%\chkstk.o
:the_end
del win%P%\*.o
@if %targetF%==PE (
@set targetF=ELF
@goto start
)
@if %B%==64 goto finished&#10;@if _%PROCESSOR_ARCHITEW6432%_==_AMD64_ goto x86_64
@if _%PROCESSOR_ARCHITECTURE%_==_AMD64_ goto x86_64
:finished
5. 使用你想要的名称保存到TCC-0.9.26,但扩展名必须是.bat。 如果使用记事本,则必须将保存对话框的类型从 “文本文档” 更改为 “所有文件”。
6. 运行脚本,确保所有内容都正确编译(请注意,可能会有来自不兼容指针类型和绑定检查的分配的警告,在特定环境中不支持malloc,但这些都可以接受),并且在win32-elf中,你应该有一个工作的elf编译器。 你还应该拥有用于32位和64位PE编译器的Win32和Win64,以及Win64-ELF中的64位ELF编译器。 注意: 64位编译器不会在非64位操作系统上编译。 如果编译TCC时出现错误,只需将“@echo off”更改为“@echo on”,然后再次运行脚本,看看哪里出了问题。
Linux
你的发行版可能已经提供了TCC的包。 如果没有,请从https://download.savannah.gnu.org/releases/tinycc/下载源代码,然后从那里继续。 你知道如何从头开始构建程序,对吗?
一个小内核示例
这个小例子以ELF格式构建了一个小内核,可以通过Grub引导。
start32.asm
; Tutorial: A small kernel with Fasm & TCC
; By Tommy.
format elf
use32
;
; Equates
;
MULTIBOOT_PAGE_ALIGN equ (1 shl 0)
MULTIBOOT_MEMORY_INFO equ (1 shl 1)
MULTIBOOT_AOUT_KLUDGE equ (1 shl 16)
MULTIBOOT_HEADER_MAGIC equ 0x1BADB002
MULTIBOOT_HEADER_FLAGS equ MULTIBOOT_PAGE_ALIGN or MULTIBOOT_MEMORY_INFO
MULTIBOOT_CHECKSUM equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
section '.text' executable
;
; Multiboot header
;
dd MULTIBOOT_HEADER_MAGIC
dd MULTIBOOT_HEADER_FLAGS
dd MULTIBOOT_CHECKSUM
;
; Kernel entry point.
;
public _start
extrn kmain
_start:
; Call the main kernel function.
call kmain
@@:
jmp @b
kernel.c
/* Tutorial: A small kernel with Fasm & TCC
* By Tommy.
*/
/*
* Main kernel function.
*/
void
kmain (void)
{
*((unsigned char *) 0xB8000) = 'H';
*((unsigned char *) 0xB8001) = 0x1F;
*((unsigned char *) 0xB8002) = 'E';
*((unsigned char *) 0xB8003) = 0x1F;
*((unsigned char *) 0xB8004) = 'L';
*((unsigned char *) 0xB8005) = 0x1F;
*((unsigned char *) 0xB8006) = 'L';
*((unsigned char *) 0xB8007) = 0x1F;
*((unsigned char *) 0xB8008) = 'O';
*((unsigned char *) 0xB8009) = 0x1F;
}
编译和链接
使用以下选项汇编start32.asm:
fasm start32.asm
编译kernel.c用:
tcc -c kernel.c
然后将整件东西链接起来:
tcc -nostdlib -Wl,-Ttext,0x100000 start32.o kernel.o -o kernel-i386.elf
如果你更喜欢二进制形式,例如,如果你正在使用自己的不支持ELF的引导加载程序,请使用以下链接方式:
tcc -nostdlib -Wl,-Ttext,0x100000 -Wl,--oformat,binary -static start32.o kernel.o -o kernel-i386.bin
就这样!
内联汇编
TCC支持内联GAS语法汇编,如GCC:
__asm__ __volatile__("hlt");
你可以用这个特性为你处理好很多事情,比如在Bochs调试:
#define breakpoint() __asm__ __volatile__("xchg %bx, %bx");
void bochs_print (char *string) {
char *c = string;
while (*c != '\0') {
outb(0xE9, *c); // may be outportb
c++;
}
}
然后将其添加到你的bochsrc中。文本编辑器中的bxrc文件:
port_e9_hack: enabled=1
magic_break: enabled=1
并从Boch的安装位置执行bochsdbg.exe,而不是bochs.exe。
关于GDT和结构体的警告
根据TCC的创始人法布里斯·贝拉德(Fabrice Bellard)的说法,并且被(痛苦地)证明是正确的: "在TCC中,只有结构字段或变量声明支持‘packed’属性,整个结构体不支持。 因此,你的解决方案是将其添加到打包结构体的每个字段中。" 因此,如果使用结构体存储GDT条目或GDTR,请注意,如果没有正确指定结构的打包,那么在加载GDT时会遇到问题。 在创建结构体时,请使用类似以下内容:
// 我们使用属性 'packed' 告诉TCC不要更改结构中的任何对齐方式。
struct some_struct {
unsigned char a __attribute__((packed));
unsigned char b __attribute__((packed));
} __attribute__((packed));
// 最后一个属性可以保留,它不会干扰编译或输出,它可能对
// 保持与GCC的兼容性有用,只要上述属性不干扰GCC即可。
而不是这样:
//我们使用属性“packed”告诉GCC不要更改结构中的任何对齐方式。
struct some_struct {
unsigned char a;
unsigned char b;
} __attribute__((packed));
内联函数警告
TCC不支持函数内联,因为 'inline' 关键字会被忽略,所以如果一个函数需要内联,你必须使用defins预定义代替。
stdint.h
TCC不包括stdint.h、 但stddef.h中提供了所需的所有typedefs类型定义 要使用stdint.h,将以下代码作为stdint.h放入你的内核包含路径中。 这将使你的代码与gcc和tcc兼容。
/* stdint.h */
#ifdef __TINYC__
/* tcc */
#include <stddef.h>
#else
/* assume gcc */
#include_next <stdint.h>
#endif