簡體   English   中英

在 C 中產生 Segfault 的最簡單的標准符合方法是什么?

[英]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 ),前提是調用sysconfposix_memalignmprotect成功。 我對 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.

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