[英]What is the simplest standard conform way to produce a Segfault in C?
我認為這個問題說明了一切。 涵蓋從 C89 到 C11 的大多數標准的示例將很有幫助。 我雖然是這個,但我想這只是未定義的行為:
#include <stdio.h>
int main( int argc, char* argv[] )
{
const char *s = NULL;
printf( "%c\n", s[0] );
return 0;
}
編輯:
正如一些投票要求澄清的那樣:我想要一個程序有一個通常的編程錯誤(我能想到的最簡單的是一個段錯誤),它(按標准)保證中止。 這與最小的段錯誤問題有點不同,它不關心這個保險。
raise()
可用於引發段錯誤:
raise(SIGSEGV);
分段錯誤是實現定義的行為。 該標准沒有定義實現應該如何處理未定義的行為,實際上實現可以優化未定義的行為並且仍然是合規的。 需要明確的是,實現定義的行為是標准未指定但實現應該記錄的行為。 未定義的行為是不可移植或錯誤的代碼,其行為不可預測,因此不能依賴。
如果我們查看C99 草案標准§3.4.3未定義的行為,它屬於第1段中的術語、定義和符號部分,它說(強調我的未來):
使用不可移植或錯誤程序結構或錯誤數據時的行為,本國際標准對此沒有要求
在第2段中說:
注意 可能的未定義行為范圍從完全忽略具有不可預測結果的情況,到在翻譯或程序執行期間以環境特征的記錄方式表現(有或沒有發出診斷消息),到終止翻譯或執行(使用發出診斷消息)。
另一方面,如果您只是想要標准中定義的方法,該方法會在大多數類 Unix系統上導致分段錯誤,那么raise(SIGSEGV)
應該可以實現該目標。 雖然嚴格來說, SIGSEGV
的定義如下:
SIGSEGV 對存儲的無效訪問
和 §7.14信號處理<signal.h>
說:
實現不需要生成任何這些信號,除非是對 raise 函數的顯式調用。 附加的信號和指向不可聲明函數的指針,宏定義分別以字母 SIG 和一個大寫字母或 SIG_ 和一個大寫字母開頭,219)也可以由實現指定。 完整的信號集、它們的語義和它們的默認處理是實現定義的; 所有信號編號都應為正數。
該標准僅提及未定義的行為。 它對內存分段一無所知。 另請注意,產生錯誤的代碼不符合標准。 您的代碼不能同時調用未定義的行為並符合標准。
盡管如此,在確實產生此類故障的架構上產生分段錯誤的最短方法是:
int main()
{
*(int*)0 = 0;
}
為什么這肯定會產生段錯誤? 因為訪問內存地址0總是被系統困住; 它永遠不可能是有效的訪問(至少不是通過用戶空間代碼。)
當然請注意,並非所有架構都以相同的方式工作。 在其中一些上,上述內容根本不會崩潰,而是會產生其他類型的錯誤。 或者該語句可能非常好,甚至可以很好地訪問內存位置 0。 這就是該標准實際上並未定義會發生什么的原因之一。
正確的程序不會產生段錯誤。 而且您無法描述不正確程序的確定性行為。
“分段錯誤”是 x86 CPU 所做的事情。 您可以通過嘗試以不正確的方式引用內存來獲得它。 它還可以指內存訪問導致頁面錯誤(即嘗試訪問未加載到頁表中的內存)並且操作系統決定您無權請求該內存的情況。 要觸發這些條件,您需要直接為您的操作系統和硬件進行編程。 它不是 C 語言指定的。
如果我們假設我們沒有發出調用raise
的信號,則分段錯誤很可能來自未定義的行為。 未定義的行為是未定義的,編譯器可以自由拒絕翻譯,因此未定義的任何答案都不能保證在所有實現上都失敗。 此外,調用未定義行為的程序是錯誤程序。
但這是我能在我的系統上得到該段錯誤的最短時間:
main(){main();}
(我用gcc
和-std=c89 -O0
編譯)。
順便說一句,這個程序真的會調用未定義的行為嗎?
main;
而已。
真的。
本質上,它的作用是將main
定義為variable 。 在C語言中,變量和函數都是符號——內存中的指針,所以編譯器不區分它們,這段代碼也不會拋出錯誤。
但是,問題在於系統如何運行可執行文件。 簡而言之,C 標准要求所有 C 可執行文件都有一個內置的環境准備入口點,這基本上歸結為“調用main
”。
然而,在這種特殊情況下, main
是一個變量,因此它被放置在一個名為.bss
的內存的不可執行部分中,用於變量(而不是.text
用於代碼)。 嘗試執行.bss
中的代碼違反了其特定的分段,因此系統會引發分段錯誤。
為了說明,這里是(部分)結果文件的objdump
:
# (unimportant)
Disassembly of section .text:
0000000000001020 <_start>:
1020: f3 0f 1e fa endbr64
1024: 31 ed xor %ebp,%ebp
1026: 49 89 d1 mov %rdx,%r9
1029: 5e pop %rsi
102a: 48 89 e2 mov %rsp,%rdx
102d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1031: 50 push %rax
1032: 54 push %rsp
1033: 4c 8d 05 56 01 00 00 lea 0x156(%rip),%r8 # 1190 <__libc_csu_fini>
103a: 48 8d 0d df 00 00 00 lea 0xdf(%rip),%rcx # 1120 <__libc_csu_init>
# This is where the program should call main
1041: 48 8d 3d e4 2f 00 00 lea 0x2fe4(%rip),%rdi # 402c <main>
1048: ff 15 92 2f 00 00 callq *0x2f92(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5>
104e: f4 hlt
104f: 90 nop
# (nice things we still don't care about)
Disassembly of section .data:
0000000000004018 <__data_start>:
...
0000000000004020 <__dso_handle>:
4020: 20 40 00 and %al,0x0(%rax)
4023: 00 00 add %al,(%rax)
4025: 00 00 add %al,(%rax)
...
Disassembly of section .bss:
0000000000004028 <__bss_start>:
4028: 00 00 add %al,(%rax)
...
# main is in .bss (variables) instead of .text (code)
000000000000402c <main>:
402c: 00 00 add %al,(%rax)
...
# aaand that's it!
PS:如果您編譯為平面可執行文件,這將不起作用。 相反,您將導致未定義的行為。
在某些平台上,如果從系統請求太多資源,符合標准的 C 程序可能會因分段錯誤而失敗。 例如,使用malloc
分配一個大對象可能看起來成功,但稍后,當訪問該對象時,它會崩潰。
請注意,這樣的程序並不嚴格符合; 符合該定義的程序必須保持在每個最低實施限制內。
否則,符合標准的 C 程序不會產生分段錯誤,因為唯一的其他方式是通過未定義的行為。
SIGSEGV
信號可以顯式引發,但標准 C 庫中沒有SIGSEGV
符號。
(在此答案中,“符合標准”的意思是:“僅使用 ISO C 標准的某些版本中描述的功能,避免未指定、實現定義或未定義的行為,但不一定限於最低實現限制。”)
考慮最少字符數的最簡單形式是:
++*(int*)0;
這個問題的大部分答案都圍繞着一個關鍵點,即: C標准不包含分段錯誤的概念。 (自 C99 以來,它包括信號編號SIGSEGV
,但它沒有定義傳遞該信號的任何情況,除了raise(SIGSEGV)
,如其他答案中所討論的不計算在內。)
因此,沒有保證會導致分段錯誤的“嚴格符合”程序(即僅使用行為完全由 C 標准定義的結構的程序)。
分段錯誤由不同的標准POSIX定義。 該程序保證在任何完全符合 POSIX.1-2008 的系統(包括內存保護和高級實時選項)上引發分段錯誤或功能等效的“總線錯誤”( SIGBUS
),前提是調用sysconf
、 posix_memalign
和mprotect
成功。 我對 C99 的解讀是,該程序具有實現定義的(不是未定義的!)行為,僅考慮該標准,因此它符合但不嚴格符合。
#define _XOPEN_SOURCE 700
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(void)
{
size_t pagesize = sysconf(_SC_PAGESIZE);
if (pagesize == (size_t)-1) {
fprintf(stderr, "sysconf: %s\n", strerror(errno));
return 1;
}
void *page;
int err = posix_memalign(&page, pagesize, pagesize);
if (err || !page) {
fprintf(stderr, "posix_memalign: %s\n", strerror(err));
return 1;
}
if (mprotect(page, pagesize, PROT_NONE)) {
fprintf(stderr, "mprotect: %s\n", strerror(errno));
return 1;
}
*(long *)page = 0xDEADBEEF;
return 0;
}
在未定義的平台上很難定義一種對程序進行分段錯誤的方法。 分段錯誤是一個松散的術語,並未針對所有平台(例如簡單的小型計算機)定義。
僅考慮支持進程的操作系統,進程可以接收到發生分段錯誤的通知。
此外,將操作系統限制為“類 unix”操作系統,進程接收 SIGSEGV 信號的可靠方法是kill(getpid(),SIGSEGV)
與大多數跨平台問題的情況一樣,每個平台可能(通常會)有不同的 seg-faulting 定義。
但實際上,當前的 mac、lin 和 win 操作系統會出現 segfault on
*(int*)0 = 0;
此外,引起段錯誤也不是壞行為。 assert()
的一些實現會導致一個 SIGSEGV 信號,該信號可能會產生一個核心文件。 當您需要屍檢時非常有用。
比導致段錯誤更糟糕的是隱藏它:
try
{
anyfunc();
}
catch (...)
{
printf("?\n");
}
它隱藏了錯誤的根源,你所要做的就是:
?
.
這是我在這里沒有提到的另一種方式:
int main() {
void (*f)(void);
f();
}
在這種情況下, f
是一個未初始化的函數指針,當您嘗試調用它時會導致分段錯誤。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.