簡體   English   中英

(為什么)正在使用未初始化的變量未定義行為?

[英](Why) is using an uninitialized variable undefined behavior?

如果我有:

unsigned int x;
x -= x;

很明顯,在這個表達式之后x應該為零,但我看的每個地方,他們都說這段代碼的行為是未定義的,而不僅僅是x的值(直到減法之前)。

兩個問題:

  • 這段代碼的行為確實未定義嗎?
    (例如,代碼是否會在兼容系統上崩潰 [或更糟]?)

  • 如果是這樣,為什么C 說行為是未定義的,而這里的x應該為零是完全清楚的?

    即在這里不定義行為有什么好處

顯然,編譯器可以簡單地使用它認為在變量中“方便”的任何垃圾值,並且它會按預期工作……這種方法有什么問題?

是的,這種行為是未定義的,但原因與大多數人所知的不同。

首先,使用未初始化的值本身並不是未定義的行為,但該值只是不確定的。 如果該值恰好是該類型的陷阱表示,那么訪問它就是 UB。 無符號類型很少有陷阱表示,所以在這方面你會相對安全。

使行為未定義的是變量的一個附加屬性,即它“可以用register聲明”,即它的地址永遠不會被占用。 此類變量被特殊對待,因為有些體系結構具有真實的 CPU 寄存器,這些寄存器具有一種“未初始化”的額外狀態,並且與類型域中的值不對應。

編輯:標准的相關短語是 6.3.2.1p2:

如果左值指定了一個自動存儲持續時間的對象,該對象可以使用寄存器存儲類聲明(從未獲取其地址),並且該對象未初始化(未使用初始化程序聲明並且在使用之前未對其進行賦值) ),行為未定義。

為了更清楚,以下代碼在所有情況下都是合法的:

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • 這里取了ab的地址,所以它們的值只是不確定的。
  • 由於unsigned char從來沒有陷阱表示是不確定的值就是不確定的任何值unsigned char可能發生。
  • 最后a必須保持值0

Edit2: ab有未指定的值:

3.19.3未指定值
本國際標准對在任何情況下選擇哪個值沒有強加要求的相關類型的有效值

C 標准為編譯器提供了很多執行優化的自由。 如果您假設一個簡單的程序模型,其中未初始化的內存被設置為某種隨機位模式,並且所有操作都按照它們的寫入順序執行,那么這些優化的結果可能會令人驚訝。

注意:下面的例子是有效的,因為x從來沒有被占用過它的地址,所以它是“類似寄存器的”。 如果x的類型具有陷阱表示,它們也將是有效的; 對於 unsigned 類型,這很少是這種情況(它需要“浪費”至少一位存儲空間,並且必須記錄在案),而對於unsigned char不可能。 如果x具有有符號類型,則實現可以定義位模式,該位模式不是 -(2 n-1 -1) 和 2 n-1 -1 之間的數字作為陷阱表示。 請參閱Jens Gustedt 的回答

編譯器嘗試將寄存器分配給變量,因為寄存器比內存快。 由於程序使用的變量可能比處理器擁有的寄存器多,編譯器執行寄存器分配,這導致不同的變量在不同的時間使用相同的寄存器。 考慮程序片段

unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */

當第 3 行被求值時, x還沒有被初始化,因此(編譯器的原因)第 3 行一定是某種僥幸,由於編譯器不夠聰明而無法弄清楚的其他條件,它不會發生。 由於第 4 行之后不使用z ,第 5 行之前也不使用x ,因此兩個變量可以使用相同的寄存器。 所以這個小程序被編譯成以下對寄存器的操作:

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;

x的最終值是r0的最終值, y的最終值是r1的最終值。 這些值是 x = -3 和 y = -4,而不是 5 和 4,如果x已正確初始化,則會發生。

有關更詳細的示例,請考慮以下代碼片段:

unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

假設編譯器檢測到該condition沒有副作用。 由於condition不會修改x ,編譯器知道循環的第一次運行不可能訪問x因為它尚未初始化。 因此循環體的第一次執行等效於x = some_value() ,無需測試條件。 編譯器可能會像您編寫的那樣編譯此代碼

unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

在編譯器內部建模的方法是考慮任何依賴於x都有任何方便的值,只要x未初始化。 因為當未初始化的變量未定義時的行為,而不是變量僅具有未指定的值時,編譯器不需要跟蹤任何方便的值之間的任何特殊數學關系。 因此編譯器可以這樣分析上面的代碼:

  • 在第一次循環迭代期間, x在計算-x未初始化。
  • -x具有未定義的行為,因此它的值是任何方便的。
  • 優化規則condition ? value : value condition ? value : value適用,所以這段代碼可以簡化為condition ; value condition ; value

當遇到您問題中的代碼時,同一個編譯器會分析,當計算x = - x時, -x的值是任何方便的。 因此可以優化分配。

我還沒有尋找具有上述行為的編譯器示例,但這是優秀編譯器嘗試進行的優化。 遇到一個我不會感到驚訝。 這是一個不太可信的例子,說明你的程序崩潰的編譯器。 (如果您在某種高級調試模式下編譯程序,這可能不是那么令人難以置信。)

這個假設的編譯器映射不同內存頁面中的每個變量並設置頁面屬性,以便從未初始化的變量中讀取會導致調用調試器的處理器陷阱。 任何對變量的賦值首先要確保它的內存頁被正常映射。 該編譯器不會嘗試執行任何高級優化——它處於調試模式,旨在輕松定位諸如未初始化變量之類的錯誤。 當計算x = - x ,右側會導致陷阱並啟動調試器。

是的,程序可能會崩潰。 例如,可能存在可能導致 CPU 中斷的陷阱表示(無法處理的特定位模式),未處理可能會導致程序崩潰。

(C11 草案中的 6.2.6.1 說)某些對象表示不需要表示對象類型的值。 如果對象的存儲值具有這樣的表示形式並且被沒有字符類型的左值表達式讀取,則行為未定義。 如果這種表示是由沒有字符類型的左值表達式修改對象的全部或任何部分的副作用產生的,則行為是未定義的。50) 這種表示稱為陷阱表示。

(此解釋僅適用於unsigned int可以具有陷阱表示的平台,這在現實世界系統中很少見;有關詳細信息,請參閱注釋,並參考導致標准當前措辭的替代和可能更常見的原因。)

(此答案針對 C 1999。對於 C 2011,請參閱 Jens Gustedt 的答案。)

C 標准並沒有說使用未初始化的自動存儲持續時間對象的值是未定義的行為。 C 1999 標准在 6.7.8 10 中說,“如果沒有明確初始化具有自動存儲持續時間的對象,則其值是不確定的。” (這一段繼續定義靜態對象是如何初始化的,所以我們關心的唯一未初始化的對象是自動對象。)

3.17.2 將“不確定值”定義為“未指定值或陷阱表示”。 3.17.3 將“未指定值”定義為“本國際標准對在任何情況下選擇哪個值沒有要求的相關類型的有效值”。

因此,如果未初始化的unsigned int x具有未指定的值,則x -= x必須產生零。 這留下了它是否可能是陷阱表示的問題。 根據 6.2.6.1 5,訪問陷阱值確實會導致未定義的行為。

某些類型的對象可能具有陷阱表示,例如浮點數的信號 NaN。 但是無符號整數是特殊的。 根據 6.2.6.2,無符號整數的 N 值位中的每一個都表示 2 的冪,並且值位的每個組合表示從 0 到 2 N -1 的值之一。 因此,無符號整數只能由於其填充位(例如奇偶校驗位)中的某些值而具有陷阱表示。

如果在您的目標平台上,unsigned int 沒有填充位,則未初始化的 unsigned int 不能具有陷阱表示,並且使用其值不會導致未定義的行為。

是的,它是未定義的。 代碼可能會崩潰。 C 表示該行為是未定義的,因為沒有特定理由對一般規則進行例外處理。 優點是與所有其他未定義行為情況相同的優點——編譯器不必輸出特殊代碼來完成這項工作。

顯然,編譯器可以簡單地使用它認為在變量中“方便”的任何垃圾值,並且它會按預期工作......這種方法有什么問題?

為什么你認為這不會發生? 這正是采取的方法。 編譯器不需要讓它工作,但不需要讓它失敗。

對於任何類型的任何變量,未初始化或由於其他原因持有不確定值,以下適用於讀取該值的代碼:

  • 如果變量具有自動存儲持續時間並且沒有獲取其地址,則代碼始終調用未定義的行為 [1]。
  • 否則,如果系統支持給定變量類型的陷阱表示,代碼總是調用未定義的行為 [2]。
  • 否則,如果沒有陷阱表示,則變量采用未指定的值。 無法保證每次讀取變量時此未指定的值都是一致的。 但是,它保證不是陷阱表示,因此保證不會調用未定義的行為 [3]。

    然后可以安全地使用該值而不會導致程序崩潰,盡管此類代碼不可移植到具有陷阱表示的系統。


[1]:C11 6.3.2.1:

如果左值指定了一個自動存儲持續時間的對象,該對象可以使用寄存器存儲類聲明(從未獲取其地址),並且該對象未初始化(未使用初始化程序聲明並且在使用之前未對其進行賦值) ),行為未定義。

[2]:C11 6.2.6.1:

某些對象表示不需要表示對象類型的值。 如果對象的存儲值具有這樣的表示形式並且被沒有字符類型的左值表達式讀取,則行為未定義。 如果這種表示是由沒有字符類型的左值表達式修改對象的全部或任何部分的副作用產生的,則行為是未定義的。50) 這種表示稱為陷阱表示。

[3] C11:

3.19.2
不確定值
未指定的值或陷阱表示

3.19.3
未指定值
本國際標准對在任何情況下選擇哪個值沒有強加要求的相關類型的有效值
注意未指定的值不能是陷阱表示。

3.19.4
陷阱表示
不需要表示對象類型值的對象表示

雖然許多答案都集中在捕獲未初始化寄存器訪問的處理器上,但即使在沒有此類陷阱的平台上,使用不特別努力利用 UB 的編譯器也會出現古怪的行為。 考慮代碼:

volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
  uint16_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z;
  return temp;  
}

像 ARM 這樣的平台的編譯器,其中除加載和存儲之外的所有指令都在 32 位寄存器上運行,可以以等效於的方式合理地處理代碼:

volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
  // Since x is never used past this point, and since the return value
  // will need to be in r0, a compiler could map temp to r0
  uint32_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z & 0xFFFF;
  return temp;  
}

如果任一 volatile 讀取產生非零值,則 r0 將加載范圍為 0...65535 的值。 否則,它將產生調用函數時所持有的任何內容(即傳遞給 x 的值),這可能不是 0..65535 范圍內的值。 該標准沒有任何術語來描述類型為 uint16_t 但其值在 0..65535 范圍之外的值的行為,只是說任何可能產生此類行為的操作都會調用 UB。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM