在我之前的一個問題之后,大多數評論都說“不要這樣做,你處于不確定狀態,你必須殺死一切并重新開始”。還有一個“安全”的解決方法。
我不明白的是為什么分段錯誤本質上是不可恢復的。
寫入受保護記憶體的時刻被捕獲 - 否則,SIGSEGV將不會發送。
如果可以捕獲寫入受保護記憶體的時刻,我不明白為什么 - 從理論上講 - 它無法在某個低級別恢復,并且無法將 SIGSEGV 轉換為標準軟體例外。
請解釋為什么在分段錯誤后程式處于不確定狀態,很明顯,在實際更改記憶體之前拋出錯誤(我可能錯了,不明白為什么)。如果它被拋出,人們可以創建一個程式來更改受保護的記憶體,一次一個位元組,出現分段錯誤,并最終重新編程內核 - 一種不存在的安全風險,正如我們所看到的世界仍然存在。
- 分段錯誤究竟何時發生(= 何時
SIGSEGV發送)? - 為什么在那之后行程處于未定義的行為狀態?
- 為什么無法恢復?
- 為什么這個解決方案避免了不可恢復的狀態?甚至嗎?
uj5u.com熱心網友回復:
分段錯誤究竟何時發生(= SIGSEGV 何時發送)?
當您嘗試訪問您無權訪問的記憶體時,例如越界訪問陣列或取消參考無效指標。信號SIGSEGV是標準化的,但不同的作業系統可能會以不同的方式實作它。“分段錯誤”主要是在*nix 系統中使用的一個術語,Windows 稱之為“訪問沖突”。
為什么在那之后行程處于未定義行為狀態?
因為程式中的一個或幾個變數沒有按預期運行。假設您有一些應該存盤多個值的陣列,但是您沒有為所有這些值分配足夠的空間。所以只有那些你分配空間的人才能正確寫入,其余的寫在陣列邊界之外可以保存任何值。作業系統究竟如何知道那些越界值對您的應用程式運行有多重要?它對他們的目的一無所知。
此外,在允許的記憶體之外寫入通常會破壞其他不相關的變數,這顯然是危險的,并可能導致任何隨機行為。此類錯誤通常難以追蹤。例如,堆疊溢位就是這種易于覆寫相鄰變數的分段錯誤,除非錯誤被保護機制捕獲。
如果我們看看沒有任何作業系統和虛擬記憶體功能,只有原始物理記憶體的“裸機”微控制器系統的行為——它們只會按照指示默默地做——例如,覆寫不相關的變數并繼續前進。如果應用程式是關鍵任務,這反過來可能會導致災難性的行為。
為什么無法恢復?
因為作業系統不知道你的程式應該做什么。
盡管在上面的“裸機”場景中,系統可能足夠智能,可以將自己置于安全模式并繼續運行。不允許汽車和醫療技術等關鍵應用只是停止或重置,因為這本身可能是危險的。他們寧愿嘗試在功能有限的情況下“跛行回家”。
為什么這個解決方案避免了不可恢復的狀態?甚至嗎?
該解決方案只是忽略錯誤并繼續進行。它不能解決導致它的問題。這是一個非常臟的補丁,通常 setjmp/longjmp 是非常危險的函式,應該出于任何目的避免使用。
我們必須意識到分段錯誤是錯誤的征兆,而不是原因。
uj5u.com熱心網友回復:
請解釋為什么在分段錯誤后程式處于未確定狀態
我認為這是你的根本誤解——SEGV 不會導致不確定狀態,它是它的一個癥狀。所以問題是(通常)程式在 SIGSEGV 發生之前處于非法的、不可恢復的狀態,并且從 SIGSEGV 恢復不會改變這一點。
- 分段錯誤究竟何時發生(= SIGSEGV 何時發送)?
SIGSEGV 發生的唯一標準方式是呼叫raise(SIGSEGV);. 如果這是 SIGSEGV 的來源,那么顯然可以通過使用 longjump 來恢復它。但這是一個在現實中從未發生過的微不足道的案例。有一些特定于平臺的做事方式可能會產生明確定義的 SEGV(例如,在 POSIX 系統上使用 mprotect),并且這些 SEGV 可能是可恢復的(但可能需要特定于平臺的恢復)。然而,與未定義行為相關的 SEGV 的危險通常意味著信號處理程式將非常仔細地檢查信號附帶的(平臺相關)資訊,以確保它是預期的。
- 為什么在那之后行程處于未定義行為狀態?
在此之前它(通常)處于未定義的行為狀態;它只是沒有被注意到。這是 C 和 C 中未定義行為的大問題——沒有與之相關的特定行為,因此可能不會立即被注意到。
- 為什么這個解決方案避免了不可恢復的狀態?甚至嗎?
它不會,它只是回到更早的某個點,但不會做任何事情來撤消甚至識別導致問題的未定義行為。
uj5u.com熱心網友回復:
當您的程式嘗試取消參考錯誤指標時,會發生段錯誤。(有關更多技術版本的資訊,請參閱下文,以及其他可能出現段錯誤的內容。)此時,您的程式已經被一個導致指標錯誤的錯誤絆倒了;試圖取消它通常不是真正的錯誤。
除非你故意做一些可能出現段錯誤的事情,并打算捕捉和處理這些情況(見下面的部分),否則你不會知道程式中的錯誤(或宇宙射線有點翻轉)之前搞砸了什么錯誤的訪問實際上是錯誤的。 (這通常需要用 asm 撰寫,或運行您自己 JIT 的代碼,而不是 C 或 C 。)
C 和 C 沒有定義導致分段錯誤的程式的行為,因此編譯器不會生成預期嘗試恢復的機器代碼。即使在手寫的 asm 程式中,嘗試也沒有意義,除非您預計會出現某些型別的段錯誤,沒有理智的方法可以嘗試真正恢復;至多你應該在退出前列印一條錯誤資訊。
如果您在嘗試訪問的訪問方式的任何地址處映射一些新記憶體,或者將其從只讀變為讀 寫(在 SIGSEGV 處理程式中),則可以讓錯誤指令執行,但這不太可能讓執行恢復. 大多數只讀記憶體都是只讀的,這是有原因的,讓某些內容寫入它不會有幫助。并且嘗試通過指標讀取某些內容可能需要獲取一些實際上位于其他地方的特定資料(或者根本不讀取,因為沒有什么可讀取的)。因此,將新的零頁映射到該地址將使執行繼續,但沒有用正確執行。在 SIGSEGV 處理程式中修改主執行緒的指令指標也是如此,因此它會在出錯指令后恢復。然后任何加載或存盤都不會發生,使用之前在暫存器中的任何垃圾(用于加載),或者 CISCadd reg, [mem]或其他類似的其他結果。
(您鏈接的捕獲 SIGSEGV 的示例取決于編譯器以明顯的方式生成機器代碼,而 setjump/longjump 取決于知道哪些代碼將發生段錯誤,并且它發生時沒有先覆寫一些有效記憶體,例如stdout資料結構在進入未映射的頁面之前, printf 取決于,就像回圈或 memcpy 可能發生的那樣。)
預期的 SIGSEGV,例如 JIT 沙箱
Java 或 Javascript 等語言(沒有未定義的行為)的 JIT 需要以明確定義的方式處理空指標取消參考,通過(Java)在客戶機中拋出 NullPointerException。
實作 Java 程式邏輯的機器代碼(由 JIT 編譯器創建,作為 JVM 的一部分)在使用前至少需要檢查一次每個參考,在任何情況下它都無法在 JIT 編譯時證明它是非空,如果它想避免出現 JITed 代碼錯誤。
但這很昂貴,因此 JIT 可能會通過允許錯誤發生在它生成的來賓 asm 中來消除一些空指標檢查,即使此類錯誤將首先捕獲到作業系統,然后才捕獲到 JVM 的 SIGSEGV 處理程式。
如果 JVM 小心地布置其生成的 asm 指令,那么任何可能的空指標 deref 都會在正確的時間發生。對其他資料的副作用,并且僅對應該發生的執行路徑產生副作用(例如,請參見@supercat 的回答),那么這是有效的。JVM 必須從信號處理程式中捕獲 SIGSEGV 和 longjmp 或任何其他資訊,以撰寫將 NullPointerException 傳遞給來賓的代碼。
但這里的關鍵部分是 JVM 假設它自己的代碼沒有錯誤,因此唯一可能“損壞”的狀態是來賓的實際狀態,而不是 JVM 的有關來賓的資料。這意味著 JVM 能夠處理來賓中發生的例外,而無需依賴可能已損壞的資料。
但是,如果來賓不期望 NullPointerException 并且因此不知道如何修復這種情況,那么它本身可能無法做太多事情。除了列印錯誤訊息并退出或重新啟動之外,它可能不應該做更多的事情。(幾乎是正常的提前編譯的 C 程式的限制。)
當然,JVM 需要檢查 SIGSEGV 的錯誤地址并準確找出它在哪個訪客代碼中,才能知道將 NullPointerException 傳遞到哪里。(哪個 catch 塊,如果有的話。)如果錯誤地址根本不在 JITed 來賓代碼中,那么 JVM 就像任何其他提前編譯的 C/C 程式一樣,并且不應該做的不僅僅是列印錯誤訊息并退出。(或raise(SIGABRT)觸發核心轉儲。)
作為一個 JIT JVM,由于您自己的邏輯中的錯誤,從意外的段錯誤中恢復并不容易。關鍵是有一個沙盒來賓,您已經確保它不會弄亂主程式,并且主機 JVM 不會出現它的錯誤。(您不能允許來賓中的“托管”代碼具有可以指向任何地方的完全野指標,例如指向來賓代碼。但這通常很好。但是您仍然可以使用空指標,使用實際上實際執行的表示如果硬體嘗試取消參考它,則會出錯。這不會讓它寫入或讀取主機的狀態。)
有關這方面的更多資訊,請參閱如果段錯誤不可恢復,為什么將其稱為故障(而不是中止)?用于段錯誤的 asm 級視圖。以及指向 JIT 技術的鏈接,這些技術讓來賓代碼出現頁面錯誤而不是執行運行時檢查:
Effective Null Pointer Check Elimination Utilizing Hardware Trap是一篇關于 Java 的研究論文,來自三位 IBM 科學家。
SableVM:6.2.4關于空指標檢查的各種架構的硬體支持
另一個技巧是將陣列的末尾放在頁面的末尾(后面是足夠大的未映射區域),因此硬體免費對每次訪問進行邊界檢查。如果您可以靜態證明索引始終為正,并且它不能大于 32 位,那么您就大功告成了。
- Implicit Java Array Bounds Checking on 64-bit Architectures. They talk about what to do when array size isn't a multiple of the page size, and other caveats.
Background: what are segfaults
The usual reason for the OS delivering SIGSEGV is after your process triggers a page fault that the OS finds is "invalid". (I.e. it's your fault, not the OS's problem, so it can't fix it by paging in data that was swapped out to disk (hard page fault) or copy-on-write or zero a new anonymous page on first access (soft page fault), and updating the hardware page tables for that virtual page to match what your process logically has mapped.).
The page-fault handler can't repair the situation because the user-space thread normally because user-space hasn't asked the OS for any memory to be mapped to that virtual address. If it did just try to resume user-space without doing anything to the page table, the same instruction would just fault again, so instead the kernel delivers a SIGSEGV. The default action for that signal is to kill the process, but if user-space has installed a signal handler it can catch it.
Other reasons include (on Linux) trying to run a privileged instruction in user-space (e.g. an x86 #GP "General Protection Fault" hardware exception), or on x86 Linux a misaligned 16-byte SSE load or store (again a #GP exception). This can happen with manually-vectorized code using _mm_load_si128 instead of loadu, or even as a result of auto-vectorization in a program with undefined behaviour: Why does unaligned access to mmap'ed memory sometimes segfault on AMD64? (Some other OSes, e.g. MacOS / Darwin, deliver SIGBUS for misaligned SSE.)
Segfaults usually only happen after your program encountered a bug
所以你的程式狀態已經搞砸了,這就是為什么有一個 NULL 指標,你希望它是非 NULL,否則無效。(例如某些形式的 use-after free,或者指標被一些不代表有效指標的位覆寫。)
如果你很幸運,它會出現段錯誤并盡早和嘈雜地失敗,盡可能接近實際的錯誤;如果您不走運(例如損壞 malloc 簿記資訊),您實際上不會在執行錯誤代碼后很久才會出現段錯誤。
uj5u.com熱心網友回復:
關于分段錯誤,您必須了解的是,它們不是問題。他們是主近乎無限憐憫的一個例子(據我大學時的一位老教授說)。分段錯誤表明某些事情非常錯誤,您的程式認為訪問沒有記憶體的記憶體是個好主意。訪問本身不是問題;問題出現在某個不確定的時間之前,當出現問題時,最終導致您的程式認為此訪問是一個好主意。在這一點上訪問不存在的記憶只是一個癥狀,但是(這是主的憐憫進入它的地方)它很容易被發現癥狀。情況可能更糟;它可能是在有記憶體的地方訪問記憶體,只是,錯誤的記憶體。作業系統無法避免這種情況。
作業系統無法弄清楚是什么導致您的程式相信如此荒謬的事情,它唯一能做的就是關閉事物,然后再以作業系統無法輕易檢測到的方式做其他瘋狂的事情。通常,大多數作業系統還提供核心轉儲(程式記憶體的保存副本),理論上可以用來確定程式認為它在做什么。這對于任何非平凡的程式來說都不是很簡單,但這就是作業系統這樣做的原因,以防萬一。
uj5u.com熱心網友回復:
雖然您的問題專門詢問分段錯誤,但真正的問題是:
如果軟體或硬體組件被命令做一些荒謬甚至不可能的事情,它應該怎么做?什么都不做?猜猜實際上需要做什么并做到這一點?或者使用某種機制(例如“拋出例外”)來停止發出無意義命令的更高級別的計算?
許多工程師多年來積累的大量經驗一致認為,最好的答案是停止整體計算,并生成可以幫助某人找出問題所在的診斷資訊。
除了非法訪問受保護或不存在的記憶體之外,“無意義命令”的其他示例包括告訴 CPU 將整數除以零或執行未解碼為任何有效指令的垃圾位元組。如果使用具有運行時型別檢查的編程語言,則嘗試呼叫未為所涉及的資料型別定義的任何操作是另一個示例。
但是為什么強制一個試圖除以零的程式崩潰會更好呢?沒有人希望他們的程式崩潰。難道我們不能將除以零定義為等于某個數字,例如零或 73?難道我們不能創建可以跳過無效指令而不會出錯的 CPU 嗎?也許我們的 CPU 還可以為從受保護或未映射的記憶體地址進行的任何讀取回傳一些特殊值,例如 -1。他們可以忽略對受保護地址的寫入。沒有更多的段錯誤!呼!
當然,這些事情都是可以做到的,但實際上并沒有什么收獲。重點是:雖然沒有人希望他們的程式崩潰,但不崩潰并不意味著成功。人們撰寫和運行計算機程式做的事情,而不是僅僅“不出事”。如果程式有足夠的錯誤來讀取或寫入隨機記憶體地址或嘗試除以零,那么它執行您真正想要的操作的可能性非常低,即使允許它繼續運行也是如此。另一方面,如果程式在嘗試瘋狂的事情時沒有停止,它最終可能會做一些您不想要的事情,例如損壞或破壞您的資料。
從歷史上看,一些編程語言被設計為總是“只做某事”以回應無意義的命令,而不是引發致命錯誤。這樣做的目的是為了對新手程式員更加友好,但結果總是很糟糕。對于您的建議,即作業系統不應因段錯誤而使程式崩潰,這同樣適用。
uj5u.com熱心網友回復:
在機器代碼級別,許多平臺允許在某些情況下“預期”分段錯誤的程式調整記憶體配置并恢復執行。這對于實作堆疊監控之類的東西可能很有用。如果需要確定應用程式曾經使用過的最大堆疊量,可以將堆疊段設定為僅允許訪問少量堆疊,然后通過調整堆疊段的邊界和恢復代碼執行。
但是,在 C 語言級別,支持此類語意將極大地阻礙優化。如果有人要寫這樣的東西:
void test(float *p, int *q)
{
float temp = *p;
if (*q = 1)
function2(temp);
}
編譯器可能會認為讀取*p和 讀取-修改-寫入 序列*q相對于彼此是未排序的,并生成僅*p在初始值*q不是 -1 的情況下讀取的代碼。如果p有效,這不會對程式行為產生任何影響,但如果p無效*p,*q則即使觸發故障的訪問是在增量之前執行的,此更改也可能導致從訪問到增量后發生的段錯誤。
對于有效且有意義地支持可恢復段錯誤的語言,它必須比 C 標準更詳細地記錄允許和不允許優化的范圍,而且我認為沒有理由期待 C 的未來版本包含此類細節的標準。
uj5u.com熱心網友回復:
它是可以恢復的,但這通常是一個壞主意。例如,Microsoft C 編譯器可以選擇將段錯誤轉換為例外。
您可以查看 Microsoft SEH 檔案,但即使他們也不建議使用它。
uj5u.com熱心網友回復:
老實說,如果我可以告訴計算機忽略分段錯誤。我不會選擇這個選項。
通常會發生分段錯誤,因為您正在取消參考空指標或解除分配的指標。當取消參考 null 時,行為是完全未定義的。當參考一個解除分配的指標時,你拉取的資料可能是舊值、隨機垃圾,或者在最壞的情況下來自另一個程式的值。在任何一種情況下,我都希望程式出現段錯誤,而不是繼續并報告垃圾計算。
uj5u.com熱心網友回復:
多年來,分段錯誤一直困擾著我。我主要在嵌入式平臺上作業,因為我們在裸機上運行,??所以沒有可以記錄核心轉儲的檔案系統。系統剛剛鎖定并死亡,也許是串行埠輸出了幾個分離字符。那些年最有啟發性的時刻之一是我意識到分段錯誤(以及類似的致命錯誤)是一件好事。體驗一個不好,但將它們放在適當的位置,就像難以避免的故障點一樣。
像這樣的故障不是輕易產生的。硬體已經盡其所能恢復,故障是硬體警告您繼續操作是危險的方式。事實上,讓整個程序/系統崩潰實際上比繼續更安全。即使在具有受保護/虛擬記憶體的系統中,在此類故障后繼續執行也會破壞系統其余部分的穩定性。
如果可以捕捉到寫入受保護記憶體的時刻
除了寫入受保護的記憶體之外,還有更多方法可以進入段錯誤。您也可以通過例如從具有無效值的指標讀取來到達那里。這要么是由于先前的記憶體損壞(損壞已經造成,因此恢復為時已晚)或由于缺少錯誤檢查代碼(您的靜態分析器和/或測驗應該已捕獲)引起的。
為什么無法恢復?
您不一定知道問題的原因或問題的嚴重程度,因此您不知道如何從中恢復。如果你的記憶已經損壞,你就不能相信任何東西。可以恢復的情況是您可以提前檢測到問題的情況,因此使用例外不是解決問題的正確方法。
請注意,其中一些型別的問題可以在其他語言(如 C#)中恢復。這些語言通常有一個額外的運行時層,它會提前檢查指標地址并在硬體產生故障之前拋出例外。但是,對于像 C 這樣的低級語言,您沒有任何這些。
為什么這個解決方案避免了不可恢復的狀態?甚至嗎?
該技術“有效”,但僅適用于人為的、簡單的用例。繼續執行與恢復不同。有問題的系統仍處于故障狀態,記憶體損壞未知,您只是選擇繼續前進,而不是聽從硬體的建議來認真對待問題。不知道你的程式在那個時候會做什么。在潛在記憶體損壞后繼續執行的程式將是攻擊者的早期圣誕禮物。
即使沒有任何記憶體損壞,該解決方案在許多不同的常見用例中也會中斷。當已經在一個代碼塊中時,您不能輸入第二個受保護的代碼塊(例如在輔助函式中)。在受保護的代碼塊之外發生的任何段錯誤都將導致跳轉到代碼中不可預測的點。這意味著每一行代碼都需要在一個保護塊中,你的代碼會令人討厭。您不能呼叫外部庫代碼,因為該代碼不使用此技術并且不會設定setjmp錨點。您的“處理程式”塊無法呼叫庫函式或執行任何涉及指標的操作,否則您可能會面臨需要無限嵌套塊的風險。某些東西(如自動變數)在longjmp.
關于關鍵任務系統(或任何系統),這里缺少一件事:在生產中的大型系統中,人們無法知道段錯誤在哪里,甚至不知道段錯誤是否存在,因此修復錯誤而不是癥狀的建議不成立。
我不同意這個想法。我見過的大多數分段錯誤都是由(直接或間接)取消參考指標引起的,而沒有先驗證它們。在使用之前檢查指標會告訴您段錯誤在哪里。將復雜的陳述句拆分my_array[ptr1->offsets[ptr2->index]]為多個陳述句,以便您也可以檢查中間指標。像 Coverity 這樣的靜態分析器可以很好地找到使用指標而不進行驗證的代碼路徑。這不會保護您免受由徹底記憶體損壞引起的段錯誤,但無論如何都無法從這種情況中恢復。
在短期實踐中,我認為我的錯誤只是訪問 null 而已。
好訊息!這整個討論沒有實際意義。指標和陣列索引可以(并且應該!)在使用之前進行驗證,提前檢查比等待問題發生并嘗試恢復要少得多。
uj5u.com熱心網友回復:
這可能不是一個完整的答案,它絕不是完整或準確的,但它不適合評論
因此,SIGSEGV當您嘗試以不應該的方式訪問記憶體時(例如在只讀時寫入或從未映射的地址范圍讀取),可能會發生這種情況。如果您對環境有足夠的了解,那么單獨的此類錯誤可能是可以恢復的。
但是,您首先要如何確定無效訪問發生的原因。
在對另一個答案的評論中,您說:
短期實踐,我認為我的錯誤只是訪問null而已。
沒有應用程式是無錯誤的,所以你為什么假設如果空指標訪問可能發生,你的應用程式不會例如也有這樣的情況,即釋放后使用或對“有效”記憶體位置的越界訪問發生,那不會立即導致錯誤或SIGSEGV.
釋放后使用或越界訪問也可能將指標修改為指向無效位置或成為 nullptr,但它也可能同時更改了記憶體中的其他位置。如果您現在僅假設指標未初始化并且您的錯誤處理僅考慮這一點,那么您將繼續處理處于不符合您的期望或編譯器之一在生成代碼時的狀態的應用程式。
在這種情況下,應用程式將 - 在最好的情況下 - 在“恢復”后不久崩潰,在最壞的情況下,一些變數具有錯誤值,但它會繼續運行這些變數。對于關鍵應用程式而言,這種疏忽可能比重新啟動它更有害。
但是,如果您知道某個操作在某些情況下可能會導致SIGSEGV您可以處理該錯誤,例如您知道記憶體地址有效,但該記憶體映射到的設備可能不完全可靠并且可能導致SIGSEGV因此,從 a 中恢復SIGSEGV可能是一種有效的方法。
uj5u.com熱心網友回復:
取決于你所說的恢復是什么意思。如果作業系統向您發送 SEGV 信號,唯一合理的恢復是清理您的程式并從一開始就旋轉另一個程式,希望不會遇到同樣的陷阱。
在作業系統結束混亂之前,您無法知道您的記憶體損壞了多少。如果您嘗試從下一條指令或某個任意恢復點繼續,您的程式可能會出現進一步的錯誤行為。
似乎許多贊成的回答都忘記了,有些應用程式可以在生產中發生段錯誤而不會出現編程錯誤。以及預期高可用性、數十年使用壽命和零維護的地方。在這些環境中,通常會在程式因任何原因崩潰時重新啟動,包括段錯誤。此外,看門狗功能用于確保程式不會陷入計劃外的無限回圈。
想想您所依賴的所有沒有重置按鈕的嵌入式設備。他們依賴不完美的硬體,因為沒有完美的硬體。軟體必須處理硬體缺陷。換句話說,軟體必須能夠抵御硬體不當行為。
嵌入式并不是唯一重要的領域。想想只處理 StackOverflow 的服務器數量。如果您查看地面上的任何一項操作,電離輻射導致單個事件擾亂的可能性很小,但如果您查看大量 24/7 全天候運行的計算機,這種可能性就變得非常重要。ECC 記憶體有助于解決此問題,但并非所有內容都可以得到保護。
uj5u.com熱心網友回復:
你的程式是一個欠終止狀態,因為 C 不能定義狀態。導致這些錯誤的錯誤是未定義的行為。這是最惡劣的不良行為。
從這些事情中恢復的關鍵問題是,作為未定義的行為,編譯器沒有義務以任何方式支持它們。特別是,它可能已經進行了優化,如果僅發生定義的行為,則可證明具有相同的效果。編譯器完全有權對行重新排序、跳過行以及執行各種花哨的技巧來使您的代碼運行得更快。它所要做的就是根據C 虛擬機模型證明效果是一樣的。
當發生未定義的行為時,所有這些都會消失。您可能會遇到困難的情況,即編譯器對操作進行了重新排序,但現在無法通過執行程式一段時間使您達到可以達到的狀態。請記住,賦值會洗掉舊值。如果分配在出現段錯誤的行之前上移,則無法恢復舊值以“展開”優化。
只要沒有發生未定義的行為,這個重新排序的代碼的行為確實與原始代碼相同。一旦發生未定義的行為,就會暴露出重新排序發生并可能改變結果的事實。
這里的權衡是速度。因為編譯器不是在蛋殼上行走,害怕某些未指定的作業系統行為,所以它可以更好地優化您的代碼。
現在,因為未定義的行為始終是未定義的行為,無論您多么希望它不是,都無法使用規范的 C 方法來處理這種情況。C 語言永遠無法引入解決此問題的方法,至少無法使其定義行為并為此付出代價。在給定的平臺和編譯器上,您可能能夠識別出這種未定義的行為實際上是由您的編譯器定義的,通常以擴展的形式。事實上,我之前鏈接的答案顯示了一種將信號轉換為例外的方法,它確實適用于至少一個平臺/編譯器對。
但它總是必須像這樣處于邊緣。C 開發人員更看重優化代碼的速度,而不是定義這種未定義的行為。
uj5u.com熱心網友回復:
當您使用術語 SIGSEGV 時,我相信您使用的是帶有作業系統的系統,并且問題發生在您的用戶應用程式中。
當應用程式獲得 SIGSEGV 時,它是記憶體訪問之前出現問題的征兆。有時它可以準確地指出事情出錯的地方,通常不會。所以出了點問題,一段時間后這個錯誤是 SIGSEGV 的原因。如果錯誤發生在“作業系統中”,我的反應是關閉系統。有一個非常特殊的例外——當作業系統具有特定的功能來檢查安裝(或可能洗掉)的存盤卡或 IO 卡時。
在用戶領域,我可能會將我的應用程式分成幾個行程。一個或多個行程將完成實際作業。另一個行程將監視作業行程并可以發現其中一個何時失敗。然后監視器行程可以發現作業行程中的 SIGSEGV,它可以重新啟動作業行程或進行故障轉移或任何在特定情況下認為合適的事情。這不會恢復實際的記憶體訪問,但可能會恢復應用程式功能。
您可以查看 Erlang 的“盡早失敗”的哲學和 OTP 庫,以獲取有關這種做事方式的更多靈感。雖然它不處理 SIGSEGV,但處理其他幾種型別的問題。
uj5u.com熱心網友回復:
您的程式無法從分段錯誤中恢復,因為它不知道任何東西處于什么狀態。
考慮這個類比。
你在緬因州有一棟漂亮的房子,有一個漂亮的前花園和一條穿過它的踏腳石小路。無論出于何種原因,您都選擇用絲帶將每塊石頭連接到下一塊(也就是說,您已將它們制成單鏈表)。
一天早上,從房子里出來,你踏上第一塊石頭,然后沿著絲帶走到第二塊,然后再到第三塊,但是當你踏上第四塊石頭時,你突然發現自己在阿爾伯克基。
現在告訴我們, -如何做你歇著那?
你的程式也有同樣的窘境。
出事壯觀錯了,但你的程式有不知道這是什么,或者什么原因造成的或如何做任何事情有用。
因此:它會崩潰并燃燒。
uj5u.com熱心網友回復:
這是絕對可能的,但這會以不太穩定的方式復制現有功能。
當程式訪問尚未由物理記憶體支持的地址時,內核將已經收到頁面錯誤例外,然后將根據現有映射分配并可能初始化頁面,然后重試違規指令。
一個假設的 SEGV 處理程式會做完全相同的事情:決定應該在這個地址映射什么,創建映射并重試指令——但不同的是,如果處理程式會導致另一個 SEGV,我們可以在這里進入無限回圈,并且檢測會很困難,因為該決定需要查看代碼 - 所以我們會在這里創建一個停止問題。
內核已經懶惰地分配記憶體頁,允許映射檔案內容并支持具有寫時復制語意的共享映射,因此從這種機制中獲得的好處不多。
uj5u.com熱心網友回復:
到目前為止,答案和評論都是通過更高級別的編程模型的鏡頭做出回應的,這從根本上限制了程式員的創造力和潛力,以方便他們。所述模型定義了它們自己的語意,并且不會出于自身原因處理分段錯誤,無論是簡單性、效率還是其他任何原因。從這個角度來看,段錯誤是一種不尋常的情況,表明程式員錯誤,無論是用戶空間程式員還是語言實作的程式員。然而,問題不在于它是否是一個好主意,也不在于詢問您對此事的任何想法。
實際上,您說的是正確的:分段錯誤是可以恢復的。您可以像任何常規信號一樣,使用sigaction. 而且,是的,您的程式肯定可以以處理分段錯誤是正常功能的方式制作。
一個障礙是分段錯誤是錯誤,而不是例外,這與處理錯誤后控制流回傳到哪里不同。具體來說,故障處理程式回傳到相同的故障指令,該指令將無限期地繼續故障。不過,這不是一個真正的問題,因為它可以手動跳過,您可能會回傳到指定的位置,您可能會嘗試修補故障指令使其正確,或者如果您信任故障代碼,您可以將所述記憶體映射到存在. 憑借對機器的正確了解,沒有什么可以阻止您,即使是那些揮舞著規格的騎士。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/381014.html
上一篇:什么是例外處理程式
