我想知道是否有可能在C或匯編中實作變數宏。
我希望至少讓 va_start() 成為一個 C 宏,但看起來這可能不可能。我看到其他不同問題的答案說,這在 C 中不可能實作,因為你必須依賴未定義的行為。
為了說明情況,我正在撰寫一個內核,我不想依賴任何特定的C89編譯器或類unix匯編器。用任何C編譯器來構建源代碼對專案來說都是很重要的。保持簡單是另一個目標,不幸的是,支持像變數引數這樣的東西在某些架構上似乎很復雜(amd64 ABI)。
我知道 __builtin_va_start(v,l), __builtin_va_arg(v, l) 等宏的存在,但是這些宏只對特定的編譯器有效?
現在我有用匯編(i386 ABI)撰寫的內核 printf(, ...) 和 panic(, ...) 例程,它設定了 va_list(堆疊上第一個 va 引數的指標)并將其傳遞給 vprintf(, va_list) ,然后使用 va_arg() 宏(用 C 撰寫)。這并不依賴于任何未定義或實作定義的行為,但我希望所有的宏都是用C語言撰寫的。
uj5u.com熱心網友回復:
總結一下。只要#include <stdarg.h>并像你平時那樣使用va_start和朋友。一個符合標準的C編譯器將支持這個,即使沒有我們通常認為的 "C庫",它也完全可以在一個必須在沒有作業系統支持的裸機上運行的內核里使用。 這也是最具可移植性的解決方案,并避免了對架構、編譯器或ABI依賴性解決方案的需求。
當然,在撰寫內核時,你已經習慣了不使用庫的設施,如
<stdio.h>、<stdlib.h>,甚至<string.h>(printf、malloc、strcpy等)的函式,或者不得不自己撰寫。但是<stdarg.h>則屬于另一個類別。 它的功能可以由編譯器提供,而不需要作業系統的支持或大量的庫代碼,并且在某種意義上,它更像是編譯器/語言的一部分,而不是 "庫"。
從C標準的角度來看,有兩種符合標準的實作(見C17第4節,"一致性")。 應用程式員大多考慮的是符合標準的托管實作,它必須提供printf和所有這些。 但是對于內核或嵌入式代碼或其他任何在裸機上運行的東西,你想要的是一個符合要求的獨立實作(我簡稱為CFI)。 非正式地講,這就是 "只有編譯器 "而沒有 "標準庫"。 但有幾個標準頭檔案的內容CFI仍然必須支持,而<stdarg.h>就是其中之一。 其他的是像<limit.h>、<stddef.h>和<stdint.h>這些東西主要是常量、宏和型別定義。
(這種相同的區別一直存在于C89,同樣保證了<stdarg.h>的可用性。)
如果你的內核是在C89的基礎上發展起來的,那么你的內核就有可能在C89的基礎上發展起來。
如果你的內核能夠與任何 CFI 構建,這幾乎是內核可移植性的黃金標準。 事實上,你很難不在某些地方使用一些特定的編譯器功能(例如,行內匯編是非常有用的)。 但是<stdarg.h>不一定是其中之一;使用它,你真的沒有放棄任何可移植性。 你可以期待它被任何針對任何給定架構的可用的編譯器所支持,這包括交叉編譯器(它將被配置為使用目標的正確頭)。 例如,在 GNU 系統中,<stdarg.h>與 gcc 編譯器本身一起運送,而不是與 glibc 標準庫一起。
作為一些進一步的保證,直到最近,Linux內核本身正是以這種方式使用<stdarg.h>。 (大約一個月前,有一個commit來創建他們自己的<linux/stdarg.h>檔案,它只是從gcc的<stdarg.h>的舊版本中復制粘貼,將宏定義為他們gcc特定的__builtin版本。 反正Linux只支持用gcc構建,所以這對他們沒有影響。 但我最好的猜測是,這樣做是出于許可的原因--提交資訊強調他們復制了一個GPL 2版本--而不是基于任何技術。
相比之下,在匯編中撰寫變數函式將自然而然地把你與特定的架構聯系在一起,如果你想移植到另一個架構,它們將成為又一個需要重寫的東西。 試圖從C語言中訪問堆疊中的變數引數,如
arg = *((int *)&fixed_arg 1),(a)依賴于ABI,(b)只有在ABI中才有可能在堆疊中傳遞引數,而現在除了x86-32,沒有多少ABI,(c)是未定義的行為,可能被一些編譯器 "誤編譯"。 最后,像__builtin_va_start這樣的東西是嚴格依賴于編譯器的(這里是指gcc和clang),而使用<stdarg.h>也不會更糟,因為gcc的<stdarg.h>只是包含像#define va_start __builtin_va_start的宏。
uj5u.com熱心網友回復:
既然你強調了內核空間,那么你想要一個用戶空間的函式,通過某種內核呼叫來實作,并且是可變的,這是否正確呢? 這有點問題;因為典型的內核入口點將控制流轉換到內核堆疊上。 你的va_start(), va_arg()實作將必須知道如何穿越到用戶的堆疊,并可能將暫存器保存區的位映射到向量中。
一個更簡單的方法是讓用戶使用函式:
int ufunc(char *fmt, ...) {
va_list v;
int n。
va_start(v, fmt);
n = __ufunc(fmt, v);
va_end(v);
return n。
}
并在內核中實作__ufunc。 傳統上,這就是execl和execv系列函式合作制作方便的介面,但只使用一個內核呼叫。
盡管如此,你的內核仍然需要處理一些用戶堆疊的作業。 例如,我可以為你的呼叫制作一個 va_list 值,導致內核讀出一些私有資料。 但是如果你能夠將 va_list 指向有效的地方,并且你對 va_arg() 提供的值所做的任何處理也是有效的,你將能夠使用編譯器提供的實作。
請注意,如果用戶程式使用了與您的內核不同的呼叫慣例,您可能會有一些麻煩。 例如,微軟忽略了 amd64 的已發布 ABI,所以這可能會導致問題。
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/320393.html
標籤:
上一篇:查找HTMLHelper、URLHelper和AJAXHelper型別的所有擴展方法
下一篇:BT指令與進位標志CF之間的聯系
