跳到主要內容

發表文章

目前顯示的是 3月, 2013的文章

尾聲-總結,一顆全新的硬碟開始

步驟 使用 fdisk 等工具查看硬碟的分區情況,確定要裝到哪個分區,記下分區的首磁區磁區號 $ bximage -q -hd -mode=flat -size=120 120m.img $ fdisk 120m.img 磁碟 120m.img: 125 MB,125411328 位元組 255 磁頭,63 磁區/磁軌,15 磁柱,總計 244944 磁區 單位 = 磁區 之於 1 * 512 = 512 位元組 磁區大小 (邏輯/實體):512 位元組 / 512 位元組 I/O 大小 (最小/最佳化):512 位元組 / 512 位元組 磁碟識別碼:0x00000000

尾聲-真正的尾聲

grub 系統已經在硬碟上順利執行了,我們仍然可以改進,讓它支持多重開機,採用現有的 grub 即可。 首先,先將開機磁區裝到 Orange'S 分區的開機磁區: dd if=boot/hdboot.bin of=$(HD) seek=`echo "obase=10;ibase=16;\`egrep -e '^ROOT_BASE' boot/include/load.inc | sed -e 's/.*0x//g'\`*200" | bc` bs=1 count=446 conv=notrunc dd if=boot/hdboot.bin of=$(HD) seek=`echo "obase=10;ibase=16;\`egrep -e '^ROOT_BASE' boot/include/load.inc | sed -e 's/.*0x//g'\`*200+1FE" | bc` skip=510 bs=1 count=2 conv=notrunc 並重新編譯(此時,硬碟 80m.img 的開機磁區還是 Orange'S 的開機磁區,可是在 80m.img5 的開機磁區也有 Orange'S 的開機磁區)。

關於Linux系統下Grub啟動流程的討論總結

關於Linux系統下Grub啟動流程的討論總結 轉貼來源: http://www.test104.com/tw/tech/603.html  全世界linuxer都知道grub是什麼東西,但對於MBR引導到grub再引導到具體作業系統的這個流程可能有不少朋友就比較迷糊了。這不,cu上一位朋友就發出了這樣一個求助貼:    假如現在一台電腦上裝了WIN2000系統,那麼我現在在裝上LINUX系統和GRUB,那麼假如把GRUB裝在主分區的話,GRUB直接引導 LINUX和WIN2000,我是可以理解的,因為MBR中是GRUB的STAGE1(對不對呢?),MBR通過檢查DPT分區資訊引導系統跳轉至DBR (活動分區),我這裏想問的活動分區是什麼時候設的呢?那麼裝GRUB到MBR裏,那原來MBR中的WIN的引導資訊是怎麼處理的呢?是不是我們假如說裝 GRUB到MBR的時候,GRUB就把GRUB所在那個區設置為了活動分區了呢?然後GRUB引導時候,MBR就找到那個活動分區找到所需要的檔,然後繼續呢?假如說把GRUB裝到其他分區(非主引導區)的話,那是怎麼樣實現GRUB先啟動的呢?不是先MBR嗎?因為裝到了其他分區,沒有改主引導區,因此主引導區還是WIN2000的引導資料啊,怎麼會GRUB先啟動了呢?這是為什麼呢?跟活動分區有關係沒有呢?我看資料上寫的是哪個系統啟動哪個系統就是活動分區,可是那樣的話,似乎就解釋不通了啊,就是最最開始這個地方一直不懂,理不清楚。

尾聲-從硬碟開機

我們要將 Orange'S 安裝到硬碟上,並實現硬碟啟動。 回憶軟碟啟動的過程: BIOS 將開機磁區讀入記憶體 0000:7C00 處 跳轉到 0000:7C00 處開始執行開機程式碼 開機程式碼從軟碟中找到 loader.bin,並將其讀入記憶體 跳轉到 loader.bin 開始執行 loader.bin 從軟碟中找到 kernel.bin,並將其讀入記憶體 跳轉到 kernel.bin 開始執行,到此可認為啟動過程結束 系統執行中 第 1 步中,係由 CMOS 來決定。第 3 步和第 5 步中, 軟碟啟動:程式碼將在軟碟中尋找 loader.bin 和 kernel.bin 硬碟啟動:需要讓開機磁區程式碼從硬碟中尋找 loader.bin 並讓 loader 從硬碟中尋找 kernel.bin 故我們必須重寫 boot.asm 和 loader.asm,讓它們讀取硬碟而不是軟碟。新的檔我們取名為 hdboot.asm 和 hdldr.asm。

尾聲-一些瑣碎的事兒

問題一:我們的系統每次啟動都是「全新」的,上次建立的文件到下一次啟動就不見了。 問題二:每次都會執行一次解開 cmd.tar 的操作。 問題三:再一次解壓縮時,可能已有包含的檔案了

記憶體管理-簡單的 shell

有了 fork() 和 exec(),我們可以實現簡單的 shell 了。目前只實現一種功能:讀取命令並執行之(如果命令存在的話)。

記憶體管理-exec

exec 係用以將現在的處理序映射替換成另一個。亦即我們可從硬碟讀取另一個可執行檔,用它替換掉剛剛被 fork 出來的子處理序。 為測試 exec,勢必先有一個簡單的可執行檔(echo)。因此,我們將製作一個類似於 C 的 Run-time:把過去可供給應用程式使用的函數單獨連結成一個檔,以後直接連結即可。 目前包括: 兩個真正的系統使用 sendrec 和 printx lib/syscall.asm 字串操作 memcopy、memset、strcpy、strlen lib/string.asm FS 的介面 lib/open.c lib/read.c lib/write.c lib/close.c lib/unlink.c MM 的介面 lib/fork.c lib/exit.c lib/wait.c SYS 的介面 lib/getpid.c 其他 lib/misc.c lib/vsprintf.c lib/printf.c 將以上檔案單獨連結成一個檔:oragescrt.a ar rcs lib/orangescrt.a lib/syscall.asm lib/string.asm \ lib/open.c lib/read.c lib/write.c lib/close.c lib/unlink.c \ lib/fork.c lib/exit.c lib/wait.c \ lib/getpid.c \ lib/misc.c lib/vsprintf.c lib/printf.c 並寫一個最簡單的 echo(與 pwd)。 最後,想「安裝」一些應用程式到我們的檔案系統中,有如下工作: 編寫應用程式,並編譯連結 將連結好的應用程式打成一個 tar 包:inst.tar 將 inst.tar 用工具 dd 寫入磁片(映射)的某段特定磁區(假設這一段的第一個扇區的磁區號為 X) 啟動系統,這時 mkfs() 會在檔案系統中建立一個新檔 cmd.tar,它的 inode 中的 i_start_sect 成員會被設為 X 在某個處理序中-比如 Init-將 cmd.tar 解包,將其中包含的檔存入文件系統

用 C# 批次控制 Word 合併列印

前由 我有全區的電話資料,問題在於我要依不同里別來製作出電話簿。結果如下圖: 單純採用合併列印無法達成我的需求。解決方法係用「功能變數」儲存上一個里別,與現在里別進行比較:若不同,則換頁。不過,這樣功能變數還蠻長的。最後,我還是採用 C# 來解決。 解決方案 用 C# 控制 WORD 中合併列印的「資料來源 Data Source」,給予不同里別的「sqlstatement」。迴圈處理不同的里別即可。但可預見其處理過程會很慢,不過還好,我可以不用在意它,有跑出結果即可。 程式碼 IList<string> areas = new List<string>() { "後壁", "侯伯", "嘉苳", "土溝", "嘉田", "嘉民", "菁豊", "崁頂", "後廍", "墨林", "菁寮", "新嘉", "頂長", "平安", "仕安", "竹新", "新東", "長安", "頂安", "福安", "烏樹" }; string root = @"D:\"; // 根目錄 string data = root + @"\data.docm"; // 資料檔(即資料來源) string template = root + @"\template.docx"; // 已設定好格式與合併欄位的 Word 檔 string output = @"d:\Final"; // 輸出之資料夾 object oMissing = System.Reflection.Missing.Va...

記憶體管理-exit 和 wait

假設處理序 P 有子處理序 A。而 A 使用 exit(),那麼 MM 將會: 告訴 FS:A 退出,請作相應處理 釋放 A 佔用的記憶體 判斷 P 是否正在 WAITING 如果是 清除 P 的 WAITING 位 向 P 發送訊息以解除阻塞(到此 P 的 wait() 函數結束) 釋放 A 的處理序表項(到此 A 的 exit() 函數結束) 如果否 設定 A 的 HANGING 位 瀏覽 proc_table[],如果發現 A 有子處理序 B,那麼 將 Init 處理序設定為 B 的父處理序(換言之,將 B 過繼給 Init) 判斷是否滿足 Init 正在 WAITING 且 B 正在 HANGING 如果是 清除 Init 的 WAITING 位 向 Init 發送訊息以解除阻塞(到此 Init 的 wait() 函數結束) 釋放 B 的處理序表項(到此 B 的 exit() 函數結束) 如果否 如果 Init 正在 WAITING 但 B 並沒有 HANGING,那麼「握手」會在將來 B 使用 exit() 時發生 如果 B 正在 HANGING 但 Init 並沒有 WAITING,那麼「握手」會在將來 Init 使用 wait() 時發生 如果 P 使用 wait(),那麼 MM 將會: 瀏覽 proc_table[],如果發現 A 是 P 的子處理序,並且它正在 HANGING,那麼 向 P 發送訊息以解除阻塞(到此 P 的 wait() 函數結束) 釋放 A 的處理序表項(到此 A 的 exit() 函數結束) 如果 P 的子處理序沒有一個再 HANGING,則 設 P 的 WAITING 位 如果 P 壓根兒沒有子處理序,則 向 P 發送訊息,訊息攜帶一個表示出錯的返回值(到此 P 的 wait() 函數結束)

記憶體管理-fork

一個新的處理序需要: 自己的程式碼、資料、堆疊 在 proc_table[] 中佔用一個位置 在 GDT 中佔用一個位置,用以存放處理序對應的 LDT 描述符號 問題在於,第一項的程式碼、資料、堆疊從何而來?傳統上,生成一個新處理序時,係直接由某個已執行的處理序處來繼承或複製。 生成子處理序的系統使用及稱之為 fork()。

檔案系統-將 TTY 納入檔案系統

寫入 TTY 跟寫入普通檔是很類似的,不同之處在於 TTY 不需要進行任何埠操作,只是寫入顯示卡記憶體即可。而對於讀出操作,則有很大的不同。TTY 收到處理序讀出請求的訊息,並不能馬上返回資料,因為此時並沒有任何輸入,而需要使用者輸入字元,等輸入結束後,TTY才可以返回給處理序。問題在於: 怎樣是「輸入結束」:是美一次鍵盤敲擊後算結束(面向字元,Raw mode)?還是等敲 Enter 才算結果(面向行,Cooked mode)?或者其它? 是否要讓檔案系統等待輸入過程結束? TTY 需要馬上向檔案系統發送訊息以示返回。而檔案系統則完全應該阻塞想要得到輸入的處理序,直到輸入結束。 讀 TTY 的過程: 假設處理序 P 要求讀取 TTY,它會發送訊息給檔案系統 檔案系統將訊息傳遞給 TTY TTY 記下發出請求的處理序號等資訊後立即返回 檔案系統此時並不對 P 接觸阻塞,因為結果尚未準備好 檔案系統一如往常等待任何處理序之請求 TTY 將鍵盤輸入複製進 P 傳入的記憶體位址 一旦遇到 Enter,TTY 就告訴檔案系統,P 的請求已被滿足 檔案系統會解除對 P 的阻塞 讀取工作結束 寫 TTY 的過程: P 發訊息給檔案系統 檔案系統傳遞給 TTY TTY 受到訊息候立即將字元寫入顯示卡記憶體(保持 P 和 FS 處理序的阻塞) 完成後發訊息給檔案系統 檔案系統在發訊息給 P 整個過程結束

檔案系統-建立檔案、與其它相關函數

file descriptor 一個檔案在檔案系統中涉及五個要素: 檔案內容(資料)所佔用的磁區 i-node i-node 在 inode-map 中佔用的一位 資料磁區在 sector-map 中佔用的一位或多位 檔案在目錄中佔有的目錄項(direntry) 建立檔案 為檔案內容(資料)分配磁區 在 inode_array 中分配一個 i-node 在 inode-map 中分配一位 在 sector-map 中分配一位或多位 在相應目錄中寫入一個目錄項(direntry) 刪除檔案 釋放 inode-map 中的相應位 釋放 sector-map 中的相應位 刪除根目錄中的目錄項

檔案系統-完善硬碟驅動並製作一個檔案系統

前篇只是完成 DEV_OPEN,必須完成後續的 DEV_READ 和 DEV_WRITE,以利檔案的操作。 「檔案系統」需要: 有地方存放 Metadata-super block 有地方紀錄磁區的使用情況-sector map 有地方來紀錄任一檔的資訊,比如佔用哪些磁區等-i-node map、inode_array (存放真正的 i-node) 有地方存放檔的索引-root 資料區 檔案系統

檔案系統-硬碟驅動程式

請先建立一顆硬碟(80MB),分為 1 個 primary partition, 1 個 extended partitions, 4 個 logical partitions。 而我在此處採用 VirtualBox,故需要將 raw file 轉成 vdi 格式。 $ bximage -hd -mode=flat -size=80 -q 80m.img $ fdisk 80m.img $ VBoxManage convertfromraw -format VDI 80m.img 80m.vdi 硬碟分割結果(自由切割)

處理序間通訊(IPC, Inter-Process Communication)

採用微核心。 IPC 分為兩種: 非同步 IPC:送完訊息就不管了 同步 IPC:發送者一直等到接收者收到訊息才放手,接收者接不到訊息就一直等著。優勢如下: 作業系統無須另外維護緩衝區來存放正在傳遞的訊息 作業系統無須保留一份訊息副本 作業系統無須維護接收佇列(發送佇列還是需要的) 發送者和接收者都可在任何時刻清晰且容易第知道訊息是否送達。 假設有處理序 A 想要向 B 發送訊息 M,其過程如下: A 首先準備好 M A 透過系統使用 sendrec,最終使用 msg_send 簡單判斷是否發生鎖死 判斷目標處理序 B 是否正在等待來自 A 的訊息 如果是:訊息被複製給 B,B被解除阻塞,繼續執行 如果否:A 被阻塞,並被加入到 B 的發送佇列中 假設有處理序 B 想要接收訊息(來自特定處理序、中斷或者任意處理序),其過程如下: B 準備一個空的訊息結構體 M,用於接收訊息 B 通過系統使用 sendrec,最終使用 msg_receive 判斷 B 是否有個來自硬體的訊息(通過 has_int_msg),如果是,且 B 準備接收來自中斷的訊息或準備接收任意訊息,則馬上準備一個訊息給 B,並返回。 如果 B 想接收來自任意處理序的訊息,則從自己的發送佇列中選取締一個(如果佇列非空的話),將其訊息複製給 M 如果 B 想接收來自特定處理序 A 的訊息,則先判斷 A 是否正在等待向 B 發送訊息,若是的話,將其訊息複製給 M 如果此時沒有任何處理序發訊息給 B,B 會被阻塞

輸入/輸出系統-printf

printf 使用過程示意圖 程式碼 include/const.h (修改) #define NR_SYS_CALL 2 include/global.h (新增) extern TASK user_proc_table[]; include/proc.h (修改) typedef struct s_proc { STACK_FRAME regs; /* process registers saved in stack frame */ u16 ldt_sel; /* gdt selector giving ldt base and limit */ DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */ int ticks; /* remained ticks */ int priority; u32 pid; /* process id passed in from MM */ char p_name[16]; /* name of the process */ int nr_tty; /* 用以指定此處理序的 TTY */ } PROCESS; /* Number of tasks & procs */ #define NR_TASKS 1 #define NR_PROCS 3 include/string.h (新增) PUBLIC int strlen(char* p_str); include/type.h (新增) typedef char * va_list; kernel/proc.c (修改) for (p = proc_table; p < proc_table+NR_TASKS+NR_PROCS; p++) { for(p=proc_table;p < proc_table+NR_TASKS+...

輸入/輸出系統-TTY

TTY任務 在 TTY 任務中執行一個迴圈,此迴圈將輪詢每一個 TTY,處理它的事件,包括從鍵盤緩衝區讀取資料、顯示字元等內容。 並非每輪詢到某個 TTY 時,箭頭所對應的全部事件都會發生,只有當某個 TTY 對應的控制台是現在控制台時,它才可以讀取鍵盤緩衝區(虛線) TTY 可以對輸入的資料作更多的處理,但在這裡,我們只把它簡化為「顯示」一項。 雖然圖中鍵盤和顯示器的畫在 TTY 的外面,但應把鍵盤和顯示器算做每一個 TTY 的一部分,它們是公用的。 執行的過程如上,則輪詢到每個 TTY 不外乎作兩件事: 處理輸入:查看是不是現在 TTY,如果是則從鍵盤緩衝區讀取資料 處理輸出:如果有要顯示的內容則要顯示它 TTY 任務程式碼示意 與先前程式實現的區別在於: 每個 TTY 都應該有自己的讀和寫的動作。所以在 keyboard_read() 內部,函數需要了解自己是被哪一個 TTY 使用。我們透過為函數傳入一個參數來作到這一點,此參數指向當前 TTY 的指針。 為了讓輸入和輸出分離,被 keyboard_read() 使用的 in_process() 不應該在直接回顯字元,而應該將回顯的任務交給 TTY 來完成,這樣,我們就需要為每個 TTY 建立一塊緩衝區,用以放置將被回顯的字元。 每個 TTY 回顯字元時操作的 CONSOLE 是不同的,故每個 TTY 都應該有一個成員來記載其對應的 CONSOLE 資訊。

輸入/輸出系統-鍵盤

敲擊鍵盤有兩個方面的含義:動作和內容。 動作可分為三類:按下、保持按住的狀態、放開 內容則是鍵盤上不同的鍵。 敲擊鍵盤所產生的編碼被稱作掃描碼(Scan Code),它分為 Make Code 和 Break Code 兩類。 當一個鍵被按下或者保持住按下時,將會產生 Make Code,當鍵盤彈起時,產生 Break Code。 除了 Pause 鍵之外,每個按鍵都對應一個 Make Code 和一個 Break Code。 當 8048 檢測到一個鍵的動作後,會把相應的掃描碼發送給 8042,8042會把它轉換成相應的 Scan code set1 掃描碼,並將其放置在輸入緩衝區中,然後 8042 告訴 8259A 產生中斷(IRQ1)。如果此時鍵盤又有新的鍵被按下,8042 將不再接收,一直到緩衝區被清空,8042 才會收到更多的掃描碼。

遲到的處理序-處理序調度

程式碼 include/proc.h /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ proc.h ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Forrest Yu, 2005 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ typedef struct s_stackframe { /* proc_ptr points here ↑ Low */ u32 gs; /* ┓ │ */ u32 fs; /* ┃ │ */ u32 es; /* ┃ │ */ u32 ds; /* ┃ │ */ u32 edi; /* ┃ │ */ u32 esi; /* ┣ pushed by save() │ */ u32 ebp; /* ┃ │ */ u32 kernel_esp; /* <- 'popad' will ignore it │ */ u32 ebx; /* ┃ ↑棧從高地址往低地址增長*/ u32 edx; /* ┃ │ */ u32 ecx; /* ┃ │ */ u32 eax; /* ┛ │ */ u32 retaddr; /* return address for assembly code save() │ */ u32 eip; /* ┓ │ */ u32 cs; /* ┃ │ */ u32 eflags; /* ┣ these are pushed by CPU during interrupt │ */ u32 ...

遲到的處理序-里程碑式的成果

Orange'S 的運轉過程

遲到的處理序-第一個處理序

處理序 宏觀:有自己的目標或功能,且又受控於處理序調度模組(就是老闆啦!) 微觀:利用系統資源、有自己的程式碼和資料,與自己的堆疊;需要被調度,就如一個人輪換著做不同工作 處理序示意圖 未雨綢繆 未來會有多個處理序,但 CPU 只有一個,亦即 CPU 通常小於處理序個數:同一時刻,總有「正在執行的」與「正在休息的」處理序。對於「正在休息的」處理序,我們必須讓它在重新醒來時記住自己掛起前的狀態,以便讓原來的任務繼續執行下去。 因此,我們需要一個資料結構紀錄一個處理序的狀態:在處理序要被掛起的時候,處理序資料被寫入這個資料結構,等到處理序重新開機時,這個資訊重新被讀出來。 資料結構,用以紀錄處理序的狀態 最簡單的處理序 處理序A執行中 時鐘中斷發生,ring1 -> ring0,時鐘中斷處理常式啟動 處理序調度,下一個應執行的處理序(假設為處理序B)被指定 處理序B被恢復,ring1 -> ring0 處理序B執行中

為了處理序,幫核心新增中斷處理吧!

作為一個作業系統,處理序是最為基本且重要的東西。不僅是一個處理序,應該是多個處理序。 然而,若要達到可控制處理序,就涉及到處理序和作業系統之間執行的轉換。 因為 CPU 只有一個,同一時刻要麼「客戶處理序」在執行,不然就是「作業系統」在執行。 因此,為達到處理序之目的,勢必要有一種控制權轉換機制,亦即「中斷」。 主要工作:設定 8259A 和建立 IDT

整理眾多、顯得有點雜亂的檔案並加上 Makefile

目前為止,檔案有些多且雜亂。 $ tree . ├── a.img ├── boot │   ├── boot.asm │   ├── include │   │   ├── fat12hdr.inc │   │   ├── load.inc │   │   └── pm.inc │   └── loader.asm ├── include │   ├── const.h │   ├── protect.h │   └── type.h ├── kernel │   ├── kernel.asm │   └── start.c └── lib    ├── kliba.asm └── string.asm 與此同時,編譯、連結的指定越來越多,我們採用 Makefile 來解決此問題。 Makefile ################ # Makefile for Orange'S ################ # Entry point of Orange'S # It must have the same value with 'KernelEntryPointPhyAddr; in load.inc! ENTRYPOINT = 0x30400 # Offset of entry point in kernel file # It depends on ENTRYPOINT ENTRYOFFSET = 0x400 # Programs, flags, etc. ASM = nasm DASM = ndisam CC = gcc LD = ld ASMBFLAGS = -I boot/include/ ASMKFLAGS = -I include/ -f elf CFLAGS = -I include/ -c -fno-builtin LDFLAGS = -s -Ttext $(ENTRYPOINT) DASMFLAG...

擴充核心-切換堆疊和GDT

接續「 重新放置核心-整理記憶體中的核心並將控制權交給它 」。 esp, GDT 等內容目前還在 Loader中,為了方便控制,我們預計將它們放進核心中。 再者,目前已經可以採用 C 語言了,因此我們將盡量避免用編譯。在此,我們透過 C 語言將 Loader 中的原 GDT 全部複製給新的 GDT,並將 gdt_ptr 中的內容換成新的 GDT 的基底位址和界限。

重新放置核心-整理記憶體中的核心並將控制權交給它

在「讓作業系統走進保護模式」中已經設計 Loader ,從開機磁區啟動 Loader,以讓 Loader 接續往後之工作。 Loader 的工作: 載入核心到記憶體 跳入保護模式 此篇,Loader 不僅完成上述工作,並將控制權交給核心。由執行結果畫面的「K」,可證實核心已經執行。

編譯和 C 同步使用

foo.asm 和 bar.c 之間的使用關係 程式碼 foo.asm ; $ nasm -f elf foo.asm -o foo.o ; $ gcc -c bar.c -o bar.o ; $ ld -s hello.o bar.o -o foobar ; $ ./foobar ; $ the 2nd one ; $ extern choose [section .data] num1st dd 3 num2nd dd 4 [section .text] global _start ; 導出此入口,以便讓連接器(ld)識別 global myprint ; 導出這個函數,讓 bar.c 使用 _start: push dword [num2nd] push dword [num1st] call choose add esp, 8 mov ebx, 0 mov eax, 1 int 0x80 ; 系統使用 ; void myprint (char *msg, int len) myprint: mov edx, [esp + 8] ; len mov ecx, [esp + 4] ; msg mov ebx, 1 mov eax, 4 int 0x80 ; 系統使用 ret bar.c void myprint (char *msg, int len); int choose (int a, int b) { if (a >= 6) myprint ("the 1st one\n", 13); else myprint ("the 2nd one\n", 13); return 0; } 步驟 編譯 foo.asm 為 ELF 格式的檔案 (nasm -f elf foo.asm foo.o) 編譯 bar.c (gcc -c bar.c -o bar.o) 連結 foo.o 與 bar.o 為一執行檔 (ld -s foo.o bar.o -o foobar) 註:ELF (Executable and Linkable Format)