簡體   English   中英

可以修改 C 中的字符串文字嗎?

[英]Can a string literal in C be modified?

我最近有一個問題,我知道在下面的代碼中初始化的常量數組的指針位於.rodata區域中,並且該區域僅可讀。 但是,我在模式 C11 中看到,寫入此內存地址行為將是未定義的。 我知道Borland的Turbo-C編譯器可以寫指針指向的地方,這是因為處理器在當時的某些系統上以實模式運行,例如MS-DOS? 還是獨立於處理器的工作模式? 是否有任何其他編譯器寫入指針並且在保護模式下使用處理器不會發生任何內存破壞故障?

#include <stdio.h>

int main(void) {
    char *st = "aaa";
    *st = 'b'; 
    return 0;
}

在 MS-DOS 中使用 Turbo-C 編譯的這段代碼中,您將能夠寫入內存

正如已經指出的那樣,嘗試修改 C 中的常量字符串會導致未定義的行為。 有幾個原因。

原因之一是該字符串可能被放置在只讀存儲器中。 這允許它在同一程序的多個實例之間共享,並且如果它所在的頁面被調出,則不需要將內存保存到磁盤(因為頁面是只讀的,因此可以稍后從可執行文件)。 如果嘗試修改它,它還可以通過給出錯誤(例如分段錯誤)來幫助檢測運行時錯誤。

另一個原因是字符串可能是共享的。 許多編譯器(例如, gcc )會注意到相同的文字字符串在編譯單元中出現不止一次,並將為其共享相同的存儲。 因此,如果程序修改了一個實例,它也可能影響其他實例。

也永遠不需要這樣做,因為使用靜態字符數組可以輕松實現相同的預期效果。 例如:

#include <stdio.h>

int main(void) {
    static char st_arr[] = "aaa";
    char *st = st_arr;
    *st = 'b'; 
    return 0;
}

這正是發布的代碼試圖做的,但沒有任何未定義的行為。 它也需要相同數量的內存。 在此示例中,字符串"aaa"用作數組初始值設定項,並且沒有任何自己的存儲空間。 數組st_arr代替了原始示例中的常量字符串,但是 (1) 它不會被放置在只讀內存中,並且 (2) 它不會與對該字符串的任何其他引用共享。 所以修改它是安全的,如果實際上這是你想要的。

是否有任何其他編譯器寫入指針並且在保護模式下使用處理器不會發生任何內存破壞故障?

一些 GCC 編譯器如何修改常量字符指針?

根據https://gcc.gnu.org/onlinedocs/gcc-3.3.6/gcc/Incompatibilities.html,GCC 3 及更早版本曾經支持gcc -fwriteable-strings以讓您編譯舊的 K&R C,這顯然是合法的. (這是 ISO C 中未定義的行為,因此是 ISO C 程序中的錯誤)。 該選項將定義 ISO C 未定義的分配行為。

GCC 3.3.6 手冊 - C 方言選項

-fwritable-strings
將字符串常量存儲在可寫數據段中,並且不要對其進行唯一化。 這是為了與假設它們可以寫入字符串常量的舊程序兼容。

寫入字符串常量是一個非常糟糕的主意; “常數”應該是常數。

GCC 4.0 刪除了該選項(發行說明); 最后一個 GCC3 系列是 2006 年 3 月的 gcc3.4.6。雖然顯然在那個版本中變得有問題

gcc -fwritable-strings會將字符串文字視為非常量匿名字符數組(請參閱@gnasher 的回答),因此它們進入.data部分而不是.rodata ,從而鏈接到映射到讀取的可執行文件的一段+寫頁面,不是只讀的。 (可執行段基本上與 x86 分段無關,它只是從可執行文件到內存的開始+范圍內存映射。)

它會禁用重復字符串合並,所以char *foo() { return "hello"; } char *foo() { return "hello"; }char *bar() { return "hello"; } char *bar() { return "hello"; }將返回不同的指針值,而不是合並相同的字符串文字。


有關的:


鏈接器選項:仍然是未定義的行為,所以可能不可行

在 GNU/Linux 上,使用ld -N ( --omagic ) 鏈接將使文本(以及數據)部分讀+寫。 這可能適用於.rodata即使現代 GNU Binutils ld.rodata放在它自己的部分(通常具有 read 但沒有exec 權限)而不是使其成為.text一部分。 .text可寫很容易成為一個安全問題:你永遠不希望一個頁面同時具有 write+exec,否則一些 bug 像緩沖區溢出可能會變成代碼注入攻擊。

要從 gcc 執行此操作,請在鏈接時使用gcc -Wl,-N將該選項傳遞給 ld。

這對編寫const對象的未定義行為沒有任何作用。 例如編譯器仍然會合並重復的字符串,所以寫入一個char *foo = "hello"; 將影響整個程序中"hello"所有其他使用,甚至跨文件。

如果你想要一些可寫的東西,使用static char foo[] = "hello"; 其中帶引號的字符串只是非常量數組的數組初始值設定項。 作為獎勵,這比static char *foo = "hello";更有效static char *foo = "hello"; 在全局范圍內,因為獲取數據的間接級別少了一層:它只是一個數組,而不是存儲在內存中的指針。

您的文字“aaa”在匿名位置生成一個包含四個 const char 'a', 'a', 'a', '\\0' 的靜態數組,並返回指向第一個 'a' 的指針,轉換為 char*。

嘗試修改四個字符中的任何一個都是未定義的行為。 未定義的行為可以做任何事情,從按預期修改字符、假裝修改字符、什么都不做或崩潰。

與 static const char anonymous[4] = { 'a', 'a', 'a', '\\0' }; 基本相同。 char* st = (char*) &anonymous [0];

您在問平台是否會導致未定義的行為被定義。 這個問題的答案是肯定的。

但是您也在詢問平台是否定義了這種行為。 事實上並非如此。

在一些優化提示下,編譯器將合並字符串常量,因此寫入一個常量將寫入該常量的其他用途。 這個編譯器我用過一次,它合並字符串的能力很強。

不要寫這個代碼。 這不好。 當您轉向更現代的平台時,您會后悔以這種風格編寫代碼。

為了補充上面的正確答案,DOS 以實模式運行,因此沒有只讀存儲器。 所有的內存都是扁平且可寫的。 因此,寫入文字在當時是明確定義的(就像在任何類型的 const 變量中一樣)。

暫無
暫無

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

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