C 如何編譯出一個不需要操作系統的二進位?


現代 OS kernel 是沒有 loader 的。就是說先需要一個 loader 把 OS kernel image 調入到預先約定的內存區域,然後再調用 kernel 的 entry function。這樣 kernel 就運行起來了。對於這樣的 kernel,針對題主的問題,從依賴性來考慮,DOS 時代的任何一個 app 都是這樣的。因為 DOS 本質來說只是一個 loader。(當然 DOS 還構建了一個 Int 21,不過從依賴角度說這個 Int 21 和一個普通函數差不多。)

其實當時以 DOS 為 loader 的真正的操作系統也不少,Windows 1.x - 3.x,還有 DOS/4G 都是如此。

至於 kernel 做到獨立,你的源代碼里不要調用諸如 system call 或者 C runtime 的函數就可以了。如果需要類似的功能,你要自己寫代碼向特定的 I/O port 或者 I/O mapping address 讀寫數據來完成。或者你和硬體開發者商量好一個 device driver 介面,讓他們寫底層的細節。

至於 loader 本身也沒什麼神奇的,因為 CPU 里有一個「硬 loader entry」,一加電就自動運行。所以你就把你的 loader 安裝到這個「硬 loader entry」指定的區域就好。普通 PC 的硬 loader 是 BIOS,現在是 UEFI。這個 BIOS/UEFI 又規定了從磁碟的什麼地方再調一個 loader。

現代編譯器一般有兩個功能:一是指定一個特定函數在生成後的文件里的偏移量,二是在生成後的文件里保存一張 symbol table,說明每個函數的偏移量。對於要求比較死的 loader,就把 entry function 設置成規定的偏移量就行。對於高級 loader,基本上不用 compiler 做特定工作,直接把 loader 配置好就行了。

如果編譯器功能實在達不到,loader 又比較傻,那 kernel 開發者就要寫自己的工具了。比如寫個程序,生成一段彙編,放到整個文件最前面(如果 loader 規定如此),讓控制流 jmp 跳到真正的 entry fucntion 位置(這個位置也可以在 build kernel 的時候用自製或者通用工具得到)。


看到GCC 可以編譯Linux內核,顯然系統內核運行時不依賴操作系統,請問如何編譯出的程序才能不需要操作系統?(當然它不能使用系統提供的高級功能) 。可能說的不是太清楚,,抱歉

任何一段self-contain的代碼都可以編譯成機器碼作為一個「kernel」跑在硬體上,如果這段代碼剛好做了「管理硬體資源」這件事,它就是個操作系統內核。否則,這就是一段不依賴操作系統的代碼。比如下面這段代碼:

void the_kernel() {

while(1);

}

它完全可以被編譯為「不依賴操作系統」的機器碼。只要將它編譯,並鏈接(通常是靜態鏈接)好,在你知道the_kernel的地址的情況下,就可以把它裝載到內存里,並開始執行。

比如:

$ gcc -c the_kernel.c

這樣會生成the_kernel.o,反彙編得到如下的結果:

$ objdump -d the_kernel.o

the_kernel.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 &:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: eb fe jmp 4 &

此時我們已經得到了一個名為the_kernel的symbol,將它作為kernel的入口:

$ ld -Ttext 0x1000 -e 0x1000 -o the_kernel the_kernel.o

這樣我們就鏈接得到了一個可執行的elf。看一下:

$ objdump -d the_kernel

the_kernel: file format elf64-x86-64

Disassembly of section .text:

0000000000001000 &:
1000: 55 push %rbp
1001: 48 89 e5 mov %rsp,%rbp
1004: eb fe jmp 1004 &

我們把程序的入口放在了0x1000處。如果你不需要elf,那麼可以:

$ ld -Ttext 0x1000 -e 0x1000 -o the_kernel -oformat binary the_kernel.o

這樣你得到的the_kernel裡面就只包含6個位元組的代碼:

55 push %rbp
48 89 e5 mov %rsp,%rbp
eb fe jmp 1004 &

只要你把它放在0x1000這個地方,然後把你的程序指向這裡,就可以執行它了。有興趣你可以寫個c程序,把這個二進位文件讀進內存,然後搞一個函數指針指向這個地址。然後調用它,你的程序就順利的死循環在這個不需要操作系統的代碼上了。

-----------------這段可以忽略,似乎跑題了--------------------

當然對於一個真正的操作系統,剛剛我要你寫的這個c程序,通常是一個彙編/C寫成的bootloader。它會做同樣的事情:讀進內存,找到入口,開始執行。

一個真正的操作系統,不會只有唯一的源代碼和唯一的函數,但是他們都會被編譯成object文件,然後鏈接起來。其他的函數都可以由你自己設計的入口(the_kernel)來調用執行。而且通常情況下會用到linker script來描述你程序的layout,而不是像上面一樣直接給ld命令行參數。

-----------------跑題結束-----------------------------------------

所以編譯一個不需要操作系統的程序,重點是:靜態鏈接所有依賴,規定程序地址空間的layout。如果上面的一堆東西你完全看不懂,你就需要去了解一下「編譯」這個概念了。編譯和鏈接其實是兩個過程,而我們通常所說的編譯把他們混在一起了。

我的意思是需不需要一個特定的編譯器,(當然那種直接生成特定格式(exe/elf...)的不算)

運行一段代碼需要的只是知道你編譯出來的東西「長什麼樣」,比如在這個文件里,哪裡是代碼,哪裡是數據,代碼應該放在內存的哪裡,數據需要放在哪裡。所以這個格式可以是elf,也可以是任何一種你自己能夠理解的格式(而你可以自己寫一個foo_loader,它能夠理解你創造的bar格式)。不過因為elf已經很完善了,所以很多時候都選擇直接編譯為elf。


(幾年前寫的博客,貼過來,再更新了些細節)

可以用gcc編譯個binary,然後用grub調用運行。

首先,為了方便運行和調試我們需要一個虛擬機。虛擬機有很多選擇,這裡用最簡單的qemu。

先用dd創建一個文件作為虛擬盤,100MB就可以了:

$ dd if=/dev/zero of=disk.img count=204800 bs=512

然後對這個虛擬磁碟進行分區:

$ fdisk disk.img

用命令n創建一個分區就可以了。通常情況下分區的起始扇區是2048。用命令w把變動寫入虛擬盤。

把這個分區虛擬成設備文件:

$ sudo losetup -o 1048576 /dev/loop1 disk.img

這裡指定了起始扇區的偏移量。2048個扇區,每個扇區512個位元組,總共是1048576位元組。

格式化:

$ sudo mkfs.ext4 /dev/loop1

掛載起來,這樣就可以方便地往裡面放kernel和grub需要的配置文件和模塊什麼的:

$ sudo mount /dev/loop1 /mnt

安裝grub:

$ sudo grub-install --boot-directory=/mnt --target=i386-pc --modules="part_msdos" disk.img

使用qemu來啟動虛擬機:

$ qemu-system-i386 -hda disk.img -m 1024 -s

這時候應該能夠看到grub的提示符了。當然現在還沒有grub菜單也沒有kernel,我們暫時先關掉虛擬機。

接下來可以為grub建立個multiboot啟動菜單:

$ sudo vi /mnt/grub/grub.cfg

製作菜單命令:

menuentry "Hello" {
  multiboot (hd0,msdos1)/kernel
  boot
}

確保數據寫回了虛擬盤:

$ sync

這時候如果你再打開虛擬機,應該就可以看到啟動菜單了,當然因為還沒有kernel,選擇菜單項後無法繼續,會提示kernel找不到。

下一步,我們用c語言從頭編寫個最簡單kernel程序。這個kernel沒有實現操作系統的基本功能。但是可以被grub裝載和運行。

kernel.c:

/* 在文件里嵌入一個簽名。Grub在multiboot時會尋找這個簽名 */
#define GRUB_MAGIC 0x1BADB002
#define GRUB_FLAGS 0x0
#define GRUB_CHECKSUM (-1 * (GRUB_MAGIC + GRUB_FLAGS))

struct grub_signature {
unsigned int magic;
unsigned int flags;
unsigned int checksum;
};

struct grub_signature gs __attribute__ ((section (".grub_sig"))) = {
GRUB_MAGIC,
GRUB_FLAGS,
GRUB_CHECKSUM
};

/* 顯示字元的函數。因為我們什麼庫都不能用,只能直接寫屏了。
0xB8000是VGA彩色字元模式的數據緩存。每個字元用兩個位元組表示。
前一個是Ascii碼,後一個代表顏色。 */
void puts( const char *s, int color ){
volatile char *buffer = (volatile char*)0xB8000;

while( *s != 0 ) {
*buffer++ = *s++;
*buffer++ = color;
}
}

/* 主函數,程序入口。最後用個死循環,把代碼指針困在那裡。*/
void main (void) {
puts("Hello World!", 0x7);
while (1) {}
}

有必要再寫個鏈接模板,確保編譯好的kernel裝載在內存地址0x100000,這裡是grub代碼最後跳轉到的區域,從這裡我們的kernel接過了接力棒。

kernel.ld:

OUTPUT_FORMAT("elf32-i386")
ENTRY(main)
SECTIONS
{
.grub_sig 0x100000 : AT(0x100000)
{
*(.grub_sig)
}
.text :
{
*(.text)
}
.data :
{
*(.data)
}
.bss :
{
*(.bss)
}
/DISCARD/ :
{
*(.comment)
*(.eh_frame)
}
}

還有一個Makefile,主要是設置一些編譯選項。

Makefile:

CC = gcc
LD = ld
CFLAGS = -std=c99 -pedantic -Wall -nostdlib -ffreestanding -m32
LDFLAGS = -T kernel.ld -nostdlib -n -melf_i386
OBJS = kernel.o
.PHONY: all
all: kernel
%.o: %.c
${CC} -c ${CFLAGS} $&< kernel: $(OBJS) kernel.ld ${LD} ${LDFLAGS} -o kernel ${OBJS} clean: rm -f ${OBJS} kernel

編譯生成kernel並放入我們的虛擬盤裡:

$ sudo cp kernel /mnt/
$ sync

再次啟動虛擬機,在啟動菜單里選擇multiboot我們的kernel,應該就能看到Hello World!的字元顯示在虛擬機屏幕上了。

如果想要調試,可以運行gdb。因為我們在啟動qemu的時候使用了-s選項,所以qemu默認會打開tcp埠1234作為gdb調試埠。在gdb中可以使用target remote tcp::1234命令來連接。試試看連接,會發現cpu一直在執行0x100066處的指令。用objdump -D kernel看下kernel的彙編代碼:

0010004c &:
10004c:55 push %ebp
10004d:89 e5 mov %esp,%ebp
10004f:83 ec 08 sub $0x8,%esp
100052:c7 44 24 04 07 00 00 movl $0x7,0x4(%esp)
100059:00
10005a:c7 04 24 68 00 10 00 movl $0x100068,(%esp)
100061:e8 a6 ff ff ff call 10000c &


100066:eb fe jmp 100066 &

0x100066處的指令正好是死循環的那條jmp指令。

(以上在Arch Linux上通過。使用的grub版本是2.02-beta2。)


首先,不能和glibc和任何依賴於操作系統的庫做鏈連接,這要求不能使用任何來自glibc或這類庫的函數,另外這要求在Makefile裡面不能和這類庫做連接//libc++也不能連接

然後→

legacy bios路徑:你至少需要在這個硬碟的mbr中預先準備好一階段bootloader,一階段bootloader是16位實模式純二進位的,會被bios載入到0x07c00h通常一階段引導程序使用彙編寫的。

uefi路徑:你需要輸出為PE的DLL格式,你需要和一些靜態庫連接,具體有啥可以參考tianocore。入口點和靜態庫有關。

你可以用EFI提供的方法。另外,/*如果BIOS是工作在在保護模式下的,那麼你需要編譯為保護模式,*/如果BIOS是工作在長模式下的,那麼你需要編譯為長模式。

推薦一個項目,

http://github.com/pbatard/uefi-simple.git

可以讓你方便得在windows下寫一個不依賴操作系統的uefi程序出來,並通過qemu+ovmf調試。建議用x86-64環境。


想要達到完全不依賴OS也不是不行,嚴格說在執行流踏入entry的第一條指令開始,只要你不call系統調用,不執行CPU的特權指令,不妨問全局變數以外的堆空間,同時保證不能有別的外設發出中斷,不能執行太久超過時間片等等等等,你就可以達成不依賴OS的願望了,以上禁忌有一項違背,那麼OS立馬介入進內核態接管控制權,你的不依賴OS的願望就破滅了(斜眼微笑)……至於你的程序是怎麼開始又是怎麼結束的,"你既想在我的地盤運行又想不受我的管束,這讓我很為難啊"。綜上所述,在一個OS主導的環境中想要不依賴OS,真的很難。

如果你是在裸機上運行,那麼@馮東大神已經說的很明白了,就不是你想不想依賴OS的問題了,壓根沒有自行車啊。這個世界本沒有OS,你第一個來你就是OS。如果你的程序在引導盤的0頭0道1扇區,OS算哪根蔥;如果你的程序在ROM的FFFF:0000地址(也就是物理地址0xFFFF0),那麼恭喜你,BIOS都得對你俯首稱臣啊。(以上調侃僅僅適用於x86結構IBMPC兼容機,原諒我比較老土)。

至於是否需要特定編譯器這個問題,忘了他吧-_-

手機碼字,木有圖湊合看吧


C語言的運行本身來說是不需要操作系統的,在內核模式下面,它有直接訪問物理內存的能力,你可以直接把一個整形的地址強制轉換成指針,來寫入某些特定的內存地址。某些特殊指令則需要直接嵌入彙編。一般將這些特殊指令封裝成函數來調用。通過專用的CPU指令(x86架構)或者設備映射到內存(MIPS之類)的方式,可以直接操作硬體。以前DOS下面實模式編程的時候還有一些寫在BIOS當中的程序,可以用INT指令調用(相當於觸發軟中斷),不知道現在還有沒有了。


沒接觸過嵌入式吧?


你可以這樣想,你裝的操作系統也是寫出來的程序,為什麼你一開機就自動跳到操作系統這個程序了?

所以計算機開機引導到你的編譯好的二進位文件,他就自動執行了。引導到操作系統操作系統就打開了。

完成引導的工作當然是由loader來做,也就是把內核load到內存,然後跳到保護模式就好了。

Last but not least,C編譯的bin文件只要被load就可以運行

推薦看看《Orange"s 一個操作系統的實現》,樓主可以用bochs+nasm搭建一個平台做做看。

ps.寫代碼的時候include別的庫會報錯哦,尤其有關輸入輸出的(嗯。。。基本都不行),因為此時沒有操作系統幫你撐腰了(沒有系統調用和中斷了,得自己寫。。)


每種cpu都事先定義了其支持的中斷,以及當中斷髮生時的跳轉地址,這些都是硬體定死的(不過硬體本身可能支持對實際的地址進行配置)。

我們會編寫一段小程序,放到上面提到的跳轉地址的位置,這段小程序做的唯一一件事就是再次跳轉的實際的中斷處理函數的位置,這就是中斷向量表。

當cpu通電開始工作後一般會觸發複位中斷,這一中斷同樣包含在上面提到的中斷向量表裡。

在複位中斷處理函數里我們會對系統(硬體,內存布局等)進行初始化,然後根據需要跳轉到應用程序的入口。

對應於有操作系統的情況,可以如此類比:

reset中斷 ------ 通知操作系統你要執行一個程序

複位中斷處理函數 ------ 操作系統為要執行的程序創建一個進程並準備運行環境

進入應用程序 ------ 跳轉到main(取決於libc的默認startup files)

總結來說就是(針對gcc):

使用-nostartfiles或是-nostdlib來禁用libc默認的startup files(-nostdlib會連標準庫函數一起禁用,在沒有操作系統的情況下大量的標準庫函數都是不可用的)

針對你的系統編寫替代startup files的初始化代碼

把編譯得到的文件放到指定的位置(通過ldscript)

當然,整個過程已經是極度簡化的,省略了載入程序到內存等步驟。


這個問題得從兩個方面來回答:

  1. 操作系統到底給程序提供了什麼支持,只有在程序本身不依賴於這些操作系統提供的支持時,才能保證程序自己是可以不需要操作系統的
  2. 程序的編譯和鏈接過程具體都在做些什麼,假設程序本身不依賴於操作系統,那麼如何利用已有編譯器來編譯出操作系統無關的程序

首先回答第一個問題:操作系統給程序提供了什麼支持

初略來說,操作系統主要實現三個大方向的功能:

  • 抽象硬體資源介面並進行管理
  • 抽象程序這個概念並進行管理
  • 提供系統層級的公共服務

那麼一個程序希望做到不依賴操作系統,那麼它就得:

自己協調並管理硬體資源,比如直接編程通過bios來獲取鍵盤按鍵輸入,輸出信息到顯卡,管理內存等等

丟掉自己是一個「程序」這個概念,記住,一個可執行的程序文件是操作系統提供的抽象。如果沒有操作系統,也就不存在一個「運行時」能夠理解一個文件是可執行,並且能夠初始化並裝載執行這個文件。當然,如果文件系統也算作操作系統的一部分的話,沒有操作系統就連文件這個概念也沒有。在這種情況下,只能自己在計算機啟動時就從bios接管所有的控制

也就是實現上圖從MBR開始後的代碼,不過這裡可以自己實現bootloader,也可以依賴一些標準的boot loader,比如GRUB。如果自己實現,那麼就得遵循和bios之間的約定,例如將程序靜態鏈接到起始地址0x7c00;如果依賴GRUB,就得遵循multiboot spec

當然,你的程序還不能依賴任何系統調用和動態庫,這意味著什麼,可以看下面這個最簡單的hello world程序來說明

#include &

int main() {
printf("Hello world!");
}

這個程序因為調用了printf,所以對libc是有依賴的,即便程序如此簡單,也沒法做到不依賴操作系統。這個地方即便使用靜態鏈接也不行,因為你首先得找到一個不依賴於操作系統的libc,常用的glibc完全依賴於Linux。如果是x86體系,可以利用bios提供的INT 10H中斷來做屏幕輸出,不過就得寫彙編,用C是不行的。

這個地方你會發現,如果你需要完成一些真正有價值的功能,你不得不做大量的工作,比如自己實現基本的io操作,自己實現標準庫等等,而這些基本功能之前都是操作系統直接或者間接提供出來的

假設你通過各種努力,最後實現了自己管理硬體,沒有「程序」概念,並不依賴於任何系統調用和動態庫的一套C代碼後,你需要解決的就是我們開始提到的第二個問題:利用已有編譯器來編譯出操作系統無關的程序

其實這個問題相對而言簡單不少,C代碼的編譯一般包含四個步驟:預處理、編譯、彙編、鏈接。其中鏈接這個步驟包含obj文件合併、地址編排、以及可執行文件格式化,而我們需要的就是在鏈接這個步驟時,基於我們的需要來控制地址編排,並且跳過可執行文件格式化這一步,生成flat binary


不限於x86的話題主可以找個單片機開發板玩玩,AVR, ARM, MSP 等等架構的都行。然後用各自的cross compiler編譯代碼,不需要操作系統就能在板子上跑。x86其實也是差不多的,只是指令集更龐大,功能更多,開發成本更高而已。


unikernel

先佔個坑,以後有空再來填。


題主的問題是實際上是編譯器的ABI問題。

Application binary interface

一般來說,不需要特定編譯器,但是不用特定編譯器就得做一些額外工作,所以往往有特定的編譯器來做相應的事情。

這裡 Software Downloads at Linaro翻到最下面的Toolchain部分,Linux的工具鏈就是編譯基於Linux操作系統的,Bare Metal是編譯基於裸機的。Bare Metal編譯的binary是需要燒寫到裸機特定地址運行的。


建議題主去看看uboot,其實一個簡單的不依賴操作系統的c程序就是一個簡單的bootloader。當然僅僅靠c,是完成不了的。因為還要對cpu及其他的硬體外設進行初始化。必須用彙編來初始化c語言環境。


參考《30天自製操作系統》

你只需要修改硬碟的數據,讓cpu啟動後運行你寫的程序就可以了。當然,你的程序最好是靜態編譯的,不包含任何動態庫的。


關於你的問題,你首先要明白一點:為什麼應用程序要依賴操作系統?主要原因是程序需要:loader 和 library。

loader會首先把程序載入到內存(只要是是具有XIP屬性的記憶體都可以,不一定是內存),然後loader會解析程序的header (PE/ELF/TE...) 找到程序的入口並根據header中的一些信息reallocate一些資源給程序,接著它會把 CS:IP 指向程序入口,程序開始執行。loader可以讓程序更flexibility。

library就是你所寫的程序需要調用的一些功能性的模塊,OS下的程序大多數調用的都是 dynamic 庫(DLL)。DLL 有個好處:大大減輕了編程的負擔(有大量已有的介面給你使用,不需要自己重新編寫)和程序的體積(調用的庫函數在DLL中,不需要再包在程序中);也有其壞處:會增加程序對OS或framework的依賴度。

OS內核不需要依賴操作系統,因為他依賴的並不是操作系統而是BIOS/UEFI service;他的作用反而是構建操作系統環境,這點你搞反了。

從整個系統的角度看:只有上電之後的「第一條」指令是直接在記憶體中執行的(XIP),在構建好基本的運行環境之後(BIOS routine, UEFI Framework),後面的 BIOS/UEFI 程序基本都有 "load" 這個過程;等到所有的統一定義的介面ready之後(BIOS:INT Routine,UEFI: service and protocol)就可以載入 OS loader了;OS loader會做一些初始化操作然後去load OS Kernel。

回到你的問題:C 如何編譯出一個不需要操作系統的二進位?通常情況是不行的,原因如下:

1. 常用編譯器 build 出來的程序都會需要 loader進行載入。只有 CPU 執行的第一條指令可以做到完全的不需要「操作系統」,其它的程序執行都會需要loader 進行load。

2. C語言需要堆棧:CPU 執行的「第一條」指令後一段時間內,堆棧是沒有準備好的;在堆棧可用之前的這些代碼需要使用彙編,用不到 C 編譯器。

以上提到的操作系統,不僅僅指通常意義上我們看到的OS (Windows/Linux),也包括 BIOS/UEFI 這種可以進行文件管理的系統。


談一下我的看法, 大神們 覺得有誤 清指出哈

C語言代碼編譯之後 變成機器碼. 但是這個機器碼顯然很難脫離操作系統而運行,理由如下:

C語言編譯後的可執行文件, 拿 exe 文件為例:

exe中的代碼以二進位形式存在, 這些二進位機器碼對應的彙編指令主要來自於兩部分:

1. 你編寫的代碼

2. 你的編譯器強加到你的exe中去的代碼

舉個例子:

在 TDM GCC 4.8.1 64BIT 編譯器中, 編譯一段小代碼 以及 運行結果如下圖所示

這段代碼, 明明只是一句 printf, 但實際運行, 卻還有個窗口, 也有 關閉按鈕,最小化窗口 等等.

顯然 編譯器加上去的某些不為人知的代碼.

了解這點很重要!

就是編譯器會強行給你的程序加上某些代碼, 以讓你的程序順利地在某些系統平台中運行.

那麼為了讓你的代碼在 Win系統 32 bit 中運行. 編譯器給你的程序加入了哪些代碼?

別的不說, 入口代碼肯定有.

什麼是入口代碼?

即你的程序一運行, 程序數據會被操作系統拷貝入內存.

但這些數據中,有些是codes,有些則是data, 那麼CPU該從你的那一大段數據的何處開始執行呢?

這就需要你這個程序指定一個入口. main 函數

這個入口不是給CPU看的, 是給操作系統看的.

操作系統看了你程序的指定後, 就知道該從哪個內存地址開始執行你的程序

那麼系統就會改變CPU的寄存器CS:IP, 指向你的程序的入口函數的內存地址. 開始執行你的程序.

於是 : 顯然,你的程序運行, 必須要有一個前提條件, 有一個 loader 來載入你的程序, 同時需要這個loader 來將你的入口函數的內存地址設置為 CPU 的CS:IP, 從而讓CPU來執行你的程序.

而在操作系統上, 操作系統已經為你做好了這件事情, 如果沒了操作系統.

那麼就需要 BIOS 來指定了

比如主板的 BIOS, 一啟動電腦電源,主板的 BIOS 首先被啟動, 然後BIOS讀取硬碟, 載入系統.

那麼如果這個過程中, BIOS 載入的不是系統, 而是你的程序, 那麼你的程序就被運行上去了.


gcc就行,不需要其它編譯器。

C編譯出的二進位文件本來就可以裸運行。。只是你沒法載入啊。。。

首先要用彙編寫一個bootloader把二進位程序拷進內存,然後把PC指過去(jmp)。

參考文獻:《Orange"s一個操作系統的實現》


可以了解一下unikernel


在不使用操作系統提供的功能的時候 這是可以的

看來還是要拿著東西出來啊

在x86的機器上 你可以通過GRUB使你的 裸機程序運行起來。

首先你可能需要手動寫一個_start入口,自己準備一個堆棧 為了調用main函數 基本代碼如下(手機碼字 見諒):

// for gas

.extern main

.global _start

.section .bss

.lcomm _stack, 1024

.section .text

_start:

mov $(_stack + 1024), %esp

call main

_halt:

hlt

jmp _halt

以及確保在最終二進位文件的前8192位元組有一個符合multiboot或multiboot2規範的引導頭 (手機碼字 一時半會貼不出來 詳情可以使用搜索引擎)

通常把它寫進.text段_start前方 雖然這樣並不能保證引導頭一定在文件的前8192位元組

然後

使用C語言寫一個不依賴系統功能的main函數 比如你可以做一個死循環 或者什麼也不做 或者通過寫顯存再屏幕上顯示Hello, Word! Hi, Excel!

寫好之後編譯所有文件,用鏈接器將它們全部鏈接起來。因為沒有開啟分頁機制 鏈接器為這個程序指定的默認入口地址很可能超出機器的物理地址範圍 為了防止這種情況發生 我們可以為其手動指定一個地址 比如0x100000

最後 在GRUB中為其添加一個啟動項 重啟機器 選擇新添加的啟動項啟動 如果不出意外 你大概可以看到Hello, Word! Hi, Excel!出現在屏幕上(如果你寫了顯存來顯示字元的話)

大概就是這樣 有什麼不完善的再補充


推薦閱讀:

如何看待 Linux 內核開發者 Sarah Sharp 宣布退出?
Linux 對比 Windows 能如何提高生產力?
Linux一定比win好嗎?為什麼我身邊的linux用戶勸說win和linux雙系統用戶放棄win?
對於將來想從事內核級開發的coder,以下兩件事情,哪一個更有意義?
學習 Linux 內核能找到工作嗎?

TAG:Linux | C編程語言 | GCC | 編譯器 | Linux內核 |