我想知道變數的初始化方式:
#include <stdio.h>
int main( void )
{
int ghosts[3];
for(int i =0 ; i < 3 ; i )
printf("%d\n",ghosts[i]);
return 0;
}
這讓我得到隨機值,例如 -12 2631 131 .. 它們來自哪里?
例如 x86-64 Linux 上的 GCC:https ://godbolt.org/z/MooEE3ncc
我有一個猜測來回答我的問題,無論如何它可能是錯誤
的:存盤器的暫存器在“清空”后會獲得 0 到 1 之間的隨機電壓,這些值會“四舍五入”為 0 或 1,這些隨機值取決于在某事上?!也許暫存器的制作方式?也許記憶體的容量以某種方式起作用?甚至可能是溫度?!
uj5u.com熱心網友回復:
被“清空”后的記憶體暫存器會獲得 0 到 1 之間的隨機電壓,
沒有什么比這更神秘的了。您只是看到上次使用這些記憶體位置時寫入的內容。
當記憶體被釋放時,它不會被清除或清空。系統只知道它是免費的,下次有人需要記憶體時,它就會被移交,舊的內容仍然存在。就像買了一輛舊車,在雜物箱里看,里面的東西并不神秘,找到一個點煙器和一只襪子只是一個驚喜。
有時在除錯環境中釋放的記憶體會被清除為某個可識別的值,以便很容易識別出您正在處理未初始化的記憶體。例如 0xccccccccccc 或者 0xdeadbeefDeadBeef
也許是一個更好的比喻。你在一家從不清洗盤子的自助餐廳吃飯,當顧客吃完后,他們把盤子放回“免費”堆上。當你去為自己服務時,你會從空閑堆中拿起頂板。您應該清潔盤子,否則您會得到以前客戶留下的東西
uj5u.com熱心網友回復:
每次運行新程式時,您的計算機都不會重新啟動或重新啟動。你的程式可以使用的記憶體或暫存器中的每一位存盤都有一個先前的指令留下的值,無論是在這個程式中還是在啟動這個程式之前的作業系統中。
如果是這種情況,例如對于微控制器,是的,存盤的每個位都可能在通電的電壓波動期間穩定為 0 或 1 狀態,除非存盤設計為在特定狀態下通電。(DRAM 上電時更可能為 0,因為它的電容器已經放電)。但是您還希望有內部 CPU 邏輯在從復位向量(記憶體地址)獲取和執行代碼的第一條指令之前執行一些歸零或設定為保證狀態;系統設計人員通常會在該物理地址上安排 ROM,而不是 RAM,因此他們可以在那里放置機器代碼的非隨機位元組。在該地址執行的代碼可能應該為所有暫存器假定隨機值。
但是您正在撰寫一個在作業系統下運行的簡單用戶空間程式,而不是在微控制器、嵌入式系統或主流主板的韌體下運行,因此當任何東西加載您的程式時,上電隨機性早已成為過去。
現代作業系統在行程啟動時將暫存器歸零,并將分配給用戶空間(包括您的堆疊空間)的記憶體頁面歸零,以避免內核資料和來自其他行程的資料的資訊泄漏。因此,這些值必須來自行程中較早發生的事情,可能來自之前運行main并使用了一些堆疊空間的動態聯結器代碼。
讀取從未初始化或分配的區域變數的值實際上并不是未定義的行為(在這種情況下,因為它不能被宣告register int ghosts[3],這是一個錯誤(Godbolt),因為ghosts[i]有效地使用了地址)參見 (為什么)正在使用未初始化的變數未定義的行為? 在這種情況下,所有 C 標準必須說的是該值是不確定的。 因此,正如您所料,它確實歸結為實施細節。
當您在沒有優化的情況下進行編譯時,編譯器甚至不會注意到 UB,因為它們不會跟蹤 C 陳述句的使用情況。(這意味著一切都被視為有點像volatile,只根據陳述句的需要將值加載到暫存器中,然后再次存盤。)
在我添加到您的問題的示例 Godbolt 鏈接中,請注意-Wall不會在 處產生任何警告-O0,并且只是從它為陣列選擇的堆疊記憶體中讀取,而無需寫入它。 因此,您的代碼正在觀察函式啟動時記憶體中的任何陳舊值。 (但正如我所說,這一定是在這個程式的早期撰寫的,通過 C 啟動代碼或動態鏈接。)
使用gcc -O2 -Wall,我們得到了我們期望的警告:warning: 'ghosts' is used uninitialized [-Wuninitialized],但它仍然從堆疊空間讀取而不寫入它。
有時 GCC 會發明一個0而不是讀取未初始化的堆疊空間,但在這種情況下不會發生。關于它如何編譯編譯器看到使用未初始化的“錯誤”并且可以發明它想要的任何值,例如讀取一些它從未寫過的暫存器而不是那個記憶體,這是零保證。例如,由于您正在呼叫 printf,GCC 可能只是在printf呼叫之間未初始化 ESI,因為這ghost[i]是在 x86-64 System V 呼叫約定中作為第二個引數傳遞的地方。
Most modern CPUs including x86 don't have any "trap representations" that would make an add instruction fault, and even if it did the C standard doesn't guarantee that the indeterminate value isn't a trap representation. But IA-64 did have a Not A Thing register result from bad speculative loads, which would trap if you tried to read it. See comments on the trap representation Q&A - Raymond Chen's article: Uninitialized garbage on ia64 can be deadly.
The ISO C rule about it being UB to read uninitialized variables that were candidates for register might be aimed at this, but with optimization enabled you could plausibly still run into this anyway if the taking of the address happens later, unless the compiler takes steps to avoid it. But ISO C defect report N1208 proposes saying that an indeterminate value can be "a value that behaves as if it were a trap representation" even for types that have no trap representations. So it seems that part of the standard doesn't fully cover ISAs like IA-64, the way real compilers can work.
Another case that's not exactly a "trap representation": note that only some object-representations (bit patterns) are valid for _Bool in mainstream ABIs, and violating that can crash your program: Does the C standard allow for an uninitialized bool to crash a program?
That's a C question, but I verified that GCC will return garbage without booleanizing it to 0/1 if you write _Bool b[2] ; return b[0]; https://godbolt.org/z/jMr98547o. I think ISO C only requires that an uninitialized object has some object-representation (bit-pattern), not that it's a valid one for this object (otherwise that would be a compiler bug). For most integer types, every bit-pattern is valid and represents an integer value. Besides reading uninitialized memory, you can cause the same problem using (unsigned char*) or memcpy to write a bad byte into a _Bool.
uj5u.com熱心網友回復:
我將使用一個易于查看正在發生的事情的平臺。編譯器和平臺的作業方式相同,獨立于架構、作業系統等。當然也有例外......
在 main 我要呼叫這個函式
test();
哪個是
extern void hexstring ( unsigned int );
void test ( void )
{
unsigned int x[3];
hexstring(x[0]);
hexstring(x[1]);
hexstring(x[2]);
}
hexstring 只是一個 printf(" 8X\n",x);
Build it (not using x86, using something that is overall easier to read for this demonstration)
test.c: In function ‘test’:
test.c:7:2: warning: ‘x[0]’ is used uninitialized in this function [-Wuninitialized]
7 | hexstring(x[0]);
| ^~~~~~~~~~~~~~~
test.c:8:2: warning: ‘x[1]’ is used uninitialized in this function [-Wuninitialized]
8 | hexstring(x[1]);
| ^~~~~~~~~~~~~~~
test.c:9:2: warning: ‘x[2]’ is used uninitialized in this function [-Wuninitialized]
9 | hexstring(x[2]);
| ^~~~~~~~~~~~~~~
編譯器輸出的反匯編顯示
00010134 <test>:
10134: e52de004 push {lr} ; (str lr, [sp, #-4]!)
10138: e24dd014 sub sp, sp, #20
1013c: e59d0004 ldr r0, [sp, #4]
10140: ebffffdc bl 100b8 <hexstring>
10144: e59d0008 ldr r0, [sp, #8]
10148: ebffffda bl 100b8 <hexstring>
1014c: e59d000c ldr r0, [sp, #12]
10150: e28dd014 add sp, sp, #20
10154: e49de004 pop {lr} ; (ldr lr, [sp], #4)
10158: eaffffd6 b 100b8 <hexstring>
我們可以看到堆疊區被分配了
10138: e24dd014 sub sp, sp, #20
但是然后我們直接進入閱讀和列印
1013c: e59d0004 ldr r0, [sp, #4]
10140: ebffffdc bl 100b8 <hexstring>
所以無論是在堆疊上。堆疊只是帶有特殊硬體指標的記憶體。
我們可以看到陣列中的其他兩項也被讀取(加載)和列印。
因此,此時記憶中的任何內容都會被列印出來。現在,在我們到達那里之前,我所處的環境可能會將記憶體(包括堆疊)歸零:
00000000
00000000
00000000
現在我正在優化此代碼以使其更易于閱讀,這增加了一些挑戰。
So what if we did this:
test2();
test();
In main and
void test2 ( void )
{
unsigned int y[3];
y[0]=1;
y[1]=2;
y[2]=3;
}
test2.c: In function ‘test2’:
test2.c:5:15: warning: variable ‘y’ set but not used [-Wunused-but-set-variable]
5 | unsigned int y[3];
|
and we get
00000000
00000000
00000000
but we can see why:
00010124 <test>:
10124: e52de004 push {lr} ; (str lr, [sp, #-4]!)
10128: e24dd014 sub sp, sp, #20
1012c: e59d0004 ldr r0, [sp, #4]
10130: ebffffe0 bl 100b8 <hexstring>
10134: e59d0008 ldr r0, [sp, #8]
10138: ebffffde bl 100b8 <hexstring>
1013c: e59d000c ldr r0, [sp, #12]
10140: e28dd014 add sp, sp, #20
10144: e49de004 pop {lr} ; (ldr lr, [sp], #4)
10148: eaffffda b 100b8 <hexstring>
0001014c <test2>:
1014c: e12fff1e bx lr
test didnt change but test2 is dead code as one would expect when optimized, so it did not actually touch the stack. But what if we:
test2.c
void test3 ( unsigned int * );
void test2 ( void )
{
unsigned int y[3];
y[0]=1;
y[1]=2;
y[2]=3;
test3(y);
}
test3.c
void test3 ( unsigned int *x )
{
}
Now
0001014c <test2>:
1014c: e3a01001 mov r1, #1
10150: e3a02002 mov r2, #2
10154: e3a03003 mov r3, #3
10158: e52de004 push {lr} ; (str lr, [sp, #-4]!)
1015c: e24dd014 sub sp, sp, #20
10160: e28d0004 add r0, sp, #4
10164: e98d000e stmib sp, {r1, r2, r3}
10168: eb000001 bl 10174 <test3>
1016c: e28dd014 add sp, sp, #20
10170: e49df004 pop {pc} ; (ldr pc, [sp], #4)
00010174 <test3>:
10174: e12fff1e bx lr
test2 is actually putting stuff on the stack. Now the calling conventions generally require that the stack pointer is back where it started when you were called, which means function a might move the pointer and read/write some data in that space, call function b move the pointer, read/write some data in that space, and so on. Then when each function returns it does not make sense usually to clean up, you just move the pointer back and return whatever data you wrote to that memory remains.
So if test 2 writes a few things to the stack memory space and then returns then another function is called at the same level as test2. Then the stack pointer is at the same address when test() is called as when test2() was called, in this example. So what happens?
00000001
00000002
00000003
We have managed to control what test() is printing out. Not magic.
Now rewind back to the 1960s and then work forward to the present, particularly 1980s and later.
Memory was not always cleaned up before your program ran. As some folks here are implying if you were doing banking on a spreadsheet then you closed that program and opened this program...back in the day...you would almost expect to see some data from that spreadsheet program, maybe the binary maybe the data, maybe something else, due to the nature of the operating systems use of memory it may be a fragment of the last program you ran, and a fragment of the one before that, and a fragment of a program still running that just did a free(), and so on.
Naturally, once we started to get connected to each other and hackers wanted to take over and send themselves your info or do other bad things, you can see how trivial it would be to write a program to look for passwords or bank accounts or whatever.
So not only do we have protections today to prevent one program sniffing around in another programs space, we generally assume that, today, before our program gets some memory that was used by some other program, it is wiped.
But if you disassemble even a simple hello world printf program you will see that there is a fair amount of bootstrap code that happens before main() is called. As far as the operating system is concerned, all of that code is part of our one program so even if (lets assume) memory were zeroed or cleaned before the OS loads and launches our program. Before main, within our program, we are using the stack memory to do stuff, leaving behind values, that a function like test() will see.
You may find that each time you run the same binary, one compile many runs, that the "random" data is the same. Now you may find that if you add some other shared library call or something to the overall program, then maybe, maybe, that shared library stuff causes extra code pre-main to happen to try to be able to call the shared code, or maybe as the program runs it takes different paths now because of a side effect of a change to the overall binary and now the random values are different but consistent.
There are explanations why the values could be different each time from the same binary as well.
There is no ghost in the machine though. Stack is just memory, not uncommon when a computer boots to wipe that memory once if for no other reason than to set the ecc bits. After that that memory gets reused and reused and reused and reused. And depending on the overall architecture of the operating system. How the compiler builds your application and shared libraries. And other factors. What happens to be in memory where the stack pointer is pointing when your program runs and you read before you write (as a rule never read before you write, and good that compilers are now throwing warnings) is not necessarily random and the specific list of events that happened to get to that point, were not just random but controlled, are not values that you as the programmer may have predicted. Particularly if you do this at the main() level as you have. But be it main or seventeen levels of nested function calls, it is still just some memory that may or may not contain some stuff from before you got there. Even if the bootloader zeros memory, that is still a written zero that was left behind from some other program that came before you.
毫無疑問,編譯器具有與堆疊相關的功能,這些功能可能會在呼叫結束時執行更多作業,例如在呼叫結束時歸零或預先歸零或出于安全性或某人想到的其他原因。
我今天假設,當像 windows 或 linux 或 macos 這樣的作業系統運行您的程式時,它不會讓您訪問之前出現的其他程式(包含我的銀行資訊、電子郵件、密碼等的電子表格)的一些陳舊記憶體值。但是您可以簡單地撰寫一個程式來嘗試(只需 malloc() 并列印或執行與您所做的相同的事情,但要查看堆疊更大)。我還假設程式 A 沒有辦法進入同時運行的程式 B 的記憶體。至少不是在應用程式級別。沒有黑客攻擊(malloc() 和 print 在我使用該術語時不是黑客攻擊)。
uj5u.com熱心網友回復:
該陣列ghosts未初始化,并且因為它是在函式內部宣告的并且不是static(正式地,它具有自動存盤持續時間),所以它的值是不確定的。
這意味著您可以讀取任何值,并且不能保證任何特定值。
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/437333.html
