Строковые литералы: куда они идут?
Меня интересует, где строковые литералы распределяются/сохраняются.
Я нашел один интригующий ответ here, сказав:
Определение строки inline фактически внедряет данные в самой программе и не может быть изменено (некоторые компиляторы позволяют это с помощью умного трюка, не беспокойтесь).
Но это связано с С++, не говоря уже о том, что он говорит, что не беспокоить.
Я беспокоюсь. = D
Итак, мой вопрос: где и как хранится мой строковый литерал? Почему я не должен пытаться это изменить? Выполняется ли внедрение по платформе? Кто-нибудь хочет разработать "умный трюк?"
Ответы
Ответ 1
Общим методом является то, что строковые литералы помещаются в раздел "только чтение-данные", который отображается в пространстве процесса как доступный только для чтения (поэтому вы не можете его изменить).
Это зависит от платформы. Например, более простые архитектуры чипов могут не поддерживать сегменты памяти только для чтения, поэтому сегмент данных будет доступен для записи.
Вместо этого попробуйте выяснить, как сделать строковые литералы изменчивыми (он будет сильно зависеть от вашей платформы и может меняться со временем), просто используйте массивы:
char foo[] = "...";
Компилятор организует инициализацию массива из литерала, и вы можете изменить массив.
Ответ 2
На это нет ни одного ответа. Стандарты C и С++ просто говорят, что строковые литералы имеют статическую продолжительность хранения, любая попытка их модификации дает поведение undefined, а несколько строковых литералов с одним и тем же содержимым могут или не иметь общего хранилища.
В зависимости от системы, которую вы пишете, и возможностей используемого формата исполняемого файла, они могут храниться вместе с программным кодом в текстовом сегменте или у них может быть отдельный сегмент для инициализированных данных.
Определение деталей будет зависеть и от платформы: скорее всего, это инструменты, которые могут сообщать вам, где именно. Некоторые даже дадут вам контроль над такими деталями, если вы этого захотите (например, gnu ld позволяет вам предоставить script, чтобы рассказать все о том, как группировать данные, код и т.д.)
Ответ 3
Почему я не должен пытаться его изменить?
Потому что это поведение undefined. Цитата из C99 N1256 черновик 6.7.8/32 "Инициализация" :
ПРИМЕР 8: Декларация
char s[] = "abc", t[3] = "abc";
определяет "plain" char объекты массива s
и t
, элементы которых инициализируются символами строковых символов.
Это объявление идентично
char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };
Содержимое массивов может быть изменено. С другой стороны, декларация
char *p = "abc";
определяет p
с типом "указатель на char" и инициализирует его, указывая на объект с типом "массив из char" с длиной 4, элементы которого инициализируются литералом строки символов. Если попытается использовать p
для изменения содержимого массива, поведение undefined.
Куда они идут?
GCC 4.8 x86-64 ELF Ubuntu 14.04:
-
char s[]
: стек
-
char *s
:
-
.rodata
раздел объектного файла
- тот же сегмент, в котором сбрасывается раздел
.text
объектного файла, который имеет разрешения Read и Exec, но не Write
Программа:
#include <stdio.h>
int main() {
char *s = "abc";
printf("%s\n", s);
return 0;
}
Скомпилировать и декомпилировать:
gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o
Вывод содержит:
char *s = "abc";
8: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
f: 00
c: R_X86_64_32S .rodata
Итак, строка сохраняется в разделе .rodata
.
Тогда:
readelf -l a.out
Содержит (упрощен):
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000704 0x0000000000000704 R E 200000
Section to Segment mapping:
Segment Sections...
02 .text .rodata
Это означает, что компоновщик по умолчанию script сбрасывает как .text
, так и .rodata
в сегмент, который может быть выполнен, но не изменен (Flags = R E
). Попытка изменить такой сегмент приводит к segfault в Linux.
Если мы сделаем то же самое для char[]
:
char s[] = "abc";
получаем:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
поэтому он хранится в стеке (относительно %rbp
), и мы, конечно, можем его изменить.
Ответ 4
FYI, просто создавая резервные копии других ответов:
Стандарт: ISO/IEC 14882: 2003 говорит:
2,13. Строковые литералы
-
[...] Обычный строковый литерал имеет тип "массив n const char
" и статическая продолжительность хранения (3.7)
-
Являются ли все строковые литералы различными (то есть, хранятся в неперекрывающиеся объекты) определенная реализация. Эффект попытка изменить строковый литерал undefined.
Ответ 5
gcc создает раздел .rodata
, который отображается "где-то" в адресном пространстве и помечен только для чтения,
Visual С++ (cl.exe
) создает раздел .rdata
для этой же цели.
Вы можете посмотреть выходные данные из dumpbin
или objdump
(в Linux), чтобы просмотреть разделы исполняемого файла.
например.
>dumpbin vec1.exe
Microsoft (R) COFF/PE Dumper Version 8.00.50727.762
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file vec1.exe
File Type: EXECUTABLE IMAGE
Summary
4000 .data
5000 .rdata <-- here are strings and other read-only stuff.
14000 .text
Ответ 6
Это зависит от format вашего executable. Один из способов подумать о том, что если вы планируете сборку, вы можете поместить строковые литералы в сегмент данных своей программы сборки. Ваш компилятор C делает что-то подобное, но все зависит от того, какая система, из которой вы работаете, для компиляции.
Ответ 7
Строковые литералы часто выделяются для постоянной памяти, что делает их неизменными. Тем не менее, в некоторых модификаторах компиляторов возможно "умный трюк". И умный трюк - "используя указатель на символ, указывающий на память". Помните некоторые компиляторы, возможно, это не позволяет. Вот демонстрация
char *tabHeader = "Sound";
*tabHeader = 'L';
printf("%s\n",tabHeader); // Displays "Lound"
Ответ 8
Поскольку это может отличаться от компилятора к компилятору, лучшим способом является фильтрация дампа объекта для искомого строкового литерала:
objdump -s main.o | grep -B 1 str
где -s
заставляет objdump
отображать полное содержимое всех разделов, main.o
является объектным файлом, -B 1
заставляет grep
также печатать одну строку перед совпадением (чтобы вы могли видеть имя раздела) и str
- строковый литерал, который вы ищете.
С gcc на машине Windows и одной переменной, объявленной в main
как
char *c = "whatever";
работает
objdump -s main.o | grep -B 1 whatever
возвращает
Contents of section .rdata:
0000 77686174 65766572 00000000 whatever....