c - ccc-sp/riscv2os GitHub Wiki

C 語言的設計理念與技術特徵

簡介

C 語言詭譎刁鑽卻大獲成功,想要學好 C 語言,應該理解其設計理念。

由於 C 語言是 Dennis Richie 與 UNIX 發明者 Ken Thompson 合作一起建構出來的,想了解 C 語言的歷史,就得連 UNIX 一起看。

C 語言的優勢和缺點是綁在一起的,其特性如下:

  1. 速度快,很快,非常快。(優點:快,缺點:錯誤處理很差)
  2. 沒有垃圾蒐集機制 (Garbage Collection),分配記憶體得自行回收。(優點:快,缺點:麻煩又容易出錯)
  3. 有指標 (用來儲存記憶體位址),可透過指標存取任何記憶體。(優點:低階存取,缺點:指標會亂指)
  4. 編譯式 (非解譯式) (優點:執行快,缺點:需要花時間編譯)

由於上述特性,C 語言適合《底層運作》,但是卻不太適合寫《高層應用》,舉例而言:

  1. C 語言很適合寫《作業系統、虛擬機、編譯器、組譯器》等等系統軟體。
  2. 作業系統通常用 C 語言寫出來,所以 C 語言進行系統呼叫特別親切。
  3. C 語言用在寫高層應用,像是《手機 APP/網頁網站/視窗程式》等領域,語法會較繁瑣且容易出錯。

所以,學習 C 語言,通常就是為了寫《底層的系統程式》,而非《高層的應用程式》。

話說,執行很快的語言不是只有 C 語言,早期的語言像是 Fortran, Cobol, Pascal 等都執行很快。特別是 Pascal,不只執行快,特別設計的語法讓編譯奇快無比,但或許是歷史機緣不夠,也沒有提供像指標這樣的機制可以不透過組合語言就進行記憶體映射輸出入,又沒有發展出像 UNIX 這樣影響廣泛的作業系統,所以後來在與 C 語言的競爭中敗下陣來,目前使用者已經不多了。

技術特性

接著,讓我們用程式碼來講解一下 C 語言之所以崛起的一些技術特徵,這些特徵也都是優缺點綁在一起的,難以分開。

首先,看看 C 語言最重要的特徵 -- 那就是指標。

以下程式碼片段,執行完後 ch 會變成 'b'

char ch = 'a';
char *p = &ch;
*p = 'b';

這是因為 p 儲存了 ch 的位址,於是 *p='b' 就是把 ch 那格記憶體設成 'b'。

如果我們改一下程式碼,變成下列這樣:

char *p = 100;
*p = 'b';

這樣的程式碼,有可能會當掉 (造成 segmentation fault, core dump),也有可能可以執行。

因為指標 p 設為 100, *p = 'b' 就代表要在記憶體位址 100 的那格寫入 'b' 字元,但是記憶體位址 100 的地方,有可能是《其他程式的區域,或者是作業系統區域,或者是透過 MMU映射的某分頁,該分頁可能有程式或資料,或者根本不存在 ...》。

所以這樣的程式是很危險的,指標亂指所造成的危害,是 C 語言 bug 之所以很多的原因!

但是,C 語言為何要允許指標亂指呢?不能約束一下嗎?

答案是,約束指標亂指的行為,除了會造成執行效能下降 (必須插入程式碼做邊界檢查) ,也會造成 C 語言很難被用來寫《嵌入式系統或作業系統》之類的程式。

讓我們用 mini-riscv-os 的一段程式碼來說明 C 語言在嵌入式系統的用法。

來源 -- https://github.com/cccriscv/mini-riscv-os/blob/master/01-HelloOs/os.c

#include <stdint.h>

#define UART        0x10000000
#define UART_THR    (uint8_t*)(UART+0x00) // THR:transmitter holding register
#define UART_LSR    (uint8_t*)(UART+0x05) // LSR:line status register
#define UART_LSR_EMPTY_MASK 0x40          // LSR Bit 6: Transmitter empty; both the THR and LSR are empty

int lib_putc(char ch) {
	while ((*UART_LSR & UART_LSR_EMPTY_MASK) == 0);
	return *UART_THR = ch;
}

void lib_puts(char *s) {
	while (*s) lib_putc(*s++);
}

int os_main(void)
{
	lib_puts("Hello OS!\n");
	while (1) {}
	return 0;
}

以上這段程式碼對初學者而言肯定是難如登天,所以我把 *UART_THR = ch 這行改寫成如下較易懂的狀況。

char ch = 'b';
char *UART_THR;
UART_THR = 0x10000000;
*UART_THR = ch;

以上這段程式碼,會將 'b' 字元寫到 0x10000000 這個記憶體位址中,但在 RISC-V 的 virt 機器上,這個位址是 UART 傳送接收暫存器的位址,於是 'b' 字元就寫入了該暫存器。

由於 RISC-V 程式通常跑在《開發板或虛擬機》上。

  1. 若是開發板,那麼只要將該開發板用 UART 連接線 (目前通常會是 USB) 連上你的電腦主機 (稱為宿主機) (就像 Arduino 那樣),這時你就會看到宿主機上會印出一個 'b' 字元。
  2. 若是用虛擬機模擬,那麼目前通常是用 qemu,執行完成你就會看到虛擬機視窗上會印出一個 'b' 字元。

這種技術就是 C 語言的記憶體映射輸出入。

沒有這樣的記憶體映射輸出入機制,C 語言的用途就會大打折扣,成為一個不入流的語言! (或許歷史就會改變,現在就變成 Pascal 主導整個底層軟體的電腦世界了)。

透過這樣的機制,搭配 C 語言飛快的執行速度,還有 UNIX+C 成功主導了作業系統市場,整個程式世界才會走到今天這個局面,而這也是 C 語言為何重要的原因。

理解了這件事,就可以開始這趟 C 語言之旅了,我會帶領大家逐步走向《作業系統開發之路》,透過實作體驗《當年 Dennis Richie & Ken Thompson 是如何發展出 UNIX+C 的過程》。

⚠️ **GitHub.com Fallback** ⚠️