contributed by < randyuncle
>
閱讀此章節時也一併參照:
你所不知道的 C 語言: 數值系統篇
你所不知道的 C 語言: bitwise 操作
你所不知道的 C 語言: 浮點數運算
你所不知道的 C 語言: 編譯器和最佳化原理篇
char
是 signed 資料。<stdint.h>
– int32_t
, int64_t
, uint32_t
, uint64_t
, etc..union
、cast
所定義的結構體)。+
和 -
運算子的優先度比 shift 要高。ISO/IEC 9899:2023 6.2.6.2 Integer types
6. NOTE2 The sign representation defined in this document is called two’s complement. Previous revisions of this document additionally allowed other sign representations.
short int
是一個 16 位元的整數資料型態。U
或 u
字元,即代表該數為 unsigned 整數值。
1U
在不等式裡的關係,所以 C 以 unsigned 的角度去處理不等式中的所有數值,最後會回傳 True。在 C 標頭檔 limits.h
中,可以發現到 C 的有號數整數值的上下限是這樣定義的:
在對 INT_MIN 的巨集定義中,並沒有直接將其定義為 -2147483648,而是 (-INT_MAX - 1)
。
在以 32 位元為主的程式中,如果直接定義 INT_MIN 為 -2147483648,因為在 C 語言編譯器中,碰到 -X
的場合,會先找符合 X
的資料型態,再將其 negative,所以對 -2147483648 的處裡就會先尋找符合 2147483648 的資料型態,再將其 negative。因此,在編譯階段,編譯器會先發現到 2147483648 比常規的 int 來得大,之後就找到 long,但 long 是 32 位元的 signed,所以再接著找到可以容納的目標資料型態 unsigned。又因為 -2147483648 和 2147483648 有相同的二進位表示,所以最終就停止在 unsigned。
不過以上是 C90 的情況,如果在 C99 編譯的場合,編譯器最後找到的目標資料型態為 64 位元的 signed 資料型態 long long。
另一方面,以 64 位元為主的程式中,則會因為其是 64 位元表示,所以它的 long 會是 64 位元的大小。因此,在尋找符合 -2147483648 的資料型態地任務中,會找到 long 資料型態就停止。
換個角度來說,如果編譯 INT_MIN 為十六進位數 0x80000000 的話,那在以 32 位元或 64 位元的程式中,不管是 C90 或 C99 標準,都會找到 unsigned 後,就停止搜尋符合的資料型態。
那這會有什麼問題呢?從以上的描述,可以發現到不同的 C 語言標準,以及不同位元(N-bit)程式,甚至是不同的進位法,會因為 C 語言對數值做隱性轉換的原因,都對數值 -2147483648 有不同的資料型態定義,使得 -2147483648 成為陷阱表示法。在這種場合,該程式只能在特定的機器上運行,而不能在所有的機器普遍運作,產生嚴重的錯誤。
在 CS:APP 作者提供的公開文件中,有舉了以下程式碼為例子:
若將此程式編譯執行後,根據不同的編譯器版本,以及 word size,會得到 dpos 的結果為 0 或 1,表示十進位的數值也可能為 0 或 1;另一方面,十六進位數 hpos 的執行結果,不管編譯器版本和 word size 為哪個,皆穩定為 1。此實驗也突出了,若要定義有號二進位數的常數數值,需要格外的警惕該數值可能造成的影響。
在 2002 年 FreeBSD 裡一個名叫 getpeername()
的 library function,被回報其實作存在安全性問題。其原實作簡化為以下:
其中 memcpy()
函式的原始宣告為以下:
乍看之下可能沒感覺到有什麼問題,但如果有工程師惡意的將負數值做為 maxlen
引數代入進 copy_from_kernel()
,會發生什麼事呢?
我們假設 maxlen
的數值為負,那在對 int len
的賦值中,會因為 maxlen < KSIZE
的關係,使得 maxlen
的數值賦予給變數 len
,變數 len
成為一個負數值。
接下來,變數 len
會被帶入進函式 memcpy()
的引數 n
中。但是,引數 n
的資料型態為 size_t
,是一個 unsigned 類型的資料型態,因此原來的變數 len
會被 C 語言從 signed 隱性轉換為 unsigned,且不會改變其原本的位元表示。
此時,一個很嚴重的問題就出現了:假設此程式運行在 32 位元的裝置,因為變數 len
是負數的關係,所以其位元表示中的 sign bit 為 1。若將其隱性轉換為 unsigned 表示的話,則其數值至少為 231,遠遠超過此程式定義的上限 KSIZE
,使得 memcpy()
函式會以該規模的數值,將核心記憶體複製到 user's buffer 中,其中就會複製到只有核心才有權限讀取的記憶體空間,形成資安風險。
short sx
轉換成 int sx
,接著再由 int sx
轉成 unsigned int sx
,最後把結果 assigned 給 unsigned uy
。w
位元的整數做加法,會需要 w + 1
個位元做表示。w + 1
個位元被縮減回 w
個位元。
w
位元的整數 和 ,兩數值之加法結果為 ,且 在被 assigned 後必為 w
位元。若 或 ,則表示此非負整數的加法出現了溢位。w
位元的整數做加法,會需要 w + 1
個位元做表示。w
位元的整數 和 ,兩數值之加法結果為 ,且 s 在被 assigned 後必為 w
位元。則二補數加法的溢位會有以下情況:
考慮整數集()的情況,若要表示整數加法為群,則兩整數(、)的加法有以下的特性:
綜合以上,我們可以稱整數加法為群。
若我們增加以下的條件:
則我們可以稱整數的加法為-阿貝爾群。
若整數的加法擁有群的特性,則可額外取得以下的特性:
其中,第二小點的特性,若以整數加法的角度來看,代表說每一組的整數加法,都有一組唯一對應的整數減法。
w
位元的整數做乘法,會需要 2w
個位元做表示,並在賦予回指定變數時會被縮減回 w
位元。在 2002 年,Sun Microsystem 裡的 External data representation (XDR) library 被回報一件和乘法有關的整數溢位案例。
CA-2002-25
The xdr_array() function in the XDR library provided by Sun Microsystems contains an integer overflow that can lead to improperly sized dynamic memory allocation. Subsequent problems like buffer overflows may result, depending on how and where the vulnerable xdr_array() function is used.
以下是與當時實作相似的程式碼:
在這支程式中需要注意的是關於 void *result
指標變數的宣告:
假設這支程式運行在 32 位元的裝置中,如果有工程師將 ele_cnt = 220 +1,以及 ele_size = 212 帶入此條宣告,將會使得只有 4096 bytes 的記憶體被 allocated,而非 4,294,971,392 bytes 的記憶體,導致程式在運行到 for 迴圈時,會因為 void *result
buffer 本身無法容納所有的 void *ele_src[]
二維陣列的資料,出現損毀其他裝置中的資料結構、裝置崩潰、或以下非預期的行為:
CA-2002-25
Exploiting this vulnerability will lead to denial of service, execution of arbitrary code, or the disclosure of sensitive information.
Specific impacts reported include the ability to execute arbitrary code with root privileges (by exploiting dmispd, rpc.cmsd, or kadmind, for example). In addition, intruders who exploit the XDR overflow in MIT KRB5 kadmind may be able to gain control of a Key Distribution Center (KDC) and improperly authenticate to other services within a trusted Kerberos realm.
所以,在使用記憶體分配函式(e.g. malloc
, calloc
, etc.)時,必須注意帶入進引數的數值是否會出現溢位,且不要直接以四則運算做為引數帶入。
在本書的 2.1 節中,有介紹 C 語言支援 shift operation。通常來說,在 C 語言中,如果直接使用乘法(*
)和除法(/
),則會需要使用非常多的 clock cycle 去完成該操作,而若用 bit-level operations、addition、substraction、shifting,則會只要一個 clock cycle。因此,如果想要加速程式的運作,將乘除的程式撰寫思維轉為位元操作是最合適的選擇。
整數的乘法,可以分兩種情況探討:
x * 14
。在此例子中,被乘數的二位數表示為 1410 = 11102,得知其可拆解為 14 = 23 + 22 + 21。因此,我們可以將 x * 14
以 (x << 3) + (x << 2) + (x << 1)
做為代替。使用 2 的冪的移位乘法依舊會產生 overflow,但就如同直接使用 *
operator 一樣,C 程式會直接將多餘的位元做縮回操作。
整數的除法,
TODO:
- 建立排序演算法的 Linux kernel module 版本。
- 建立測試程式的 Linux kernel module 版本。
- 建立以數值比較為主的環境測試排序演算法,以呼應在 kernel module 測試。
- 了解亂數產生器並應用進測試中 (lab0-c, integration)。
- 了解統計分析方法 (lab0-c)。
為何 C 語言的 int 是 signed?
signed vs. unsigned
int do_something()
strcmp - https://man7.org/linux/man-pages/man3/strcmp.3.html 為何有 3 種回傳值?]
equal or not => 只有 2 種
unsigned 的風險?
一個有號數意外結果的例子。
這個例子會造成無窮迴圈,因為 sizeof 會回傳值是 unsigned int 型態,i 變數也會被轉換為 unsigned 的形式,無號數 0 再減 1 就會變為 0xFFFFFFFF 而產生無窮迴圈。
(integer promotion, C99)
TODO: 第一週測驗二: Timsort 改進
list_sort.c
的改進 (show me the code!)