Строковые литералы: куда они идут?
Меня интересует, где строковые литералы распределяются / хранятся.
Я нашел здесь один интригующий ответ:
Определение строки inline фактически встраивает данные в саму программу и не может быть изменено (некоторые компиляторы допускают это с помощью хитрого трюка, не беспокойтесь).
Но это было связано с C++, не говоря уже о том, что он говорит не беспокоить.
Я беспокоюсь. =D
Итак, мой вопрос: где и как хранится мой строковый литерал? Почему я не должен пытаться изменить это? Зависит ли реализация от платформы? Кто-нибудь хочет уточнить "умный трюк"?
8 ответов
Общепринятым методом является размещение строковых литералов в разделе "только для чтения", который отображается в пространстве процесса только для чтения (поэтому вы не можете его изменить).
Это зависит от платформы. Например, более простые архитектуры чипов могут не поддерживать сегменты памяти только для чтения, поэтому сегмент данных будет доступен для записи.
Вместо этого попробуйте найти способ сделать строковые литералы изменяемыми (это будет сильно зависеть от вашей платформы и со временем изменится), просто используйте массивы:
char foo[] = "...";
Компилятор организует инициализацию массива из литерала, и вы можете изменить массив.
Почему я не должен пытаться изменить это?
Потому что это неопределенное поведение. Цитата из проекта C99 N1256 6.7.8 / 32 "Инициализация":
ПРИМЕР 8: Декларация
char s[] = "abc", t[3] = "abc";
определяет "простые" объекты массива символов
s
а такжеt
чьи элементы инициализируются символьными строковыми литералами.Эта декларация идентична
char s[] = { 'a', 'b', 'c', '\0' }, t[] = { 'a', 'b', 'c' };
Содержимое массивов может быть изменено. С другой стороны, декларация
char *p = "abc";
определяет
p
с типом "указатель на символ" и инициализирует его, чтобы указать на объект с типом "массив символа" длиной 4, элементы которого инициализируются литералом символьной строки. Если сделана попытка использоватьp
чтобы изменить содержимое массива, поведение не определено.
Куда они идут?
GCC 4.8 x86-64 ELF Ubuntu 14.04:
char s[]
: стекchar *s
:.rodata
раздел объектного файла- тот же сегмент, где
.text
секция объектного файла получает дамп, который имеет разрешения на чтение и исполнение, но не на запись
Программа:
#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
Это означает, что скрипт компоновщика по умолчанию сбрасывает оба .text
а также .rodata
в сегмент, который может быть выполнен, но не изменен (Flags = R E
). Попытка изменить такой сегмент приводит к ошибке в Linux.
Если мы сделаем то же самое для char[]
:
char s[] = "abc";
мы получаем:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
поэтому он хранится в стеке (относительно %rbp
), и мы можем, конечно, изменить его.
Там нет ни одного ответа на это. Стандарты C и C++ просто говорят, что строковые литералы имеют статическую длительность хранения, любая попытка их изменения приводит к неопределенному поведению, а несколько строковых литералов с одинаковым содержимым могут или не могут совместно использовать одно и то же хранилище.
В зависимости от системы, для которой вы пишете, и возможностей используемого формата исполняемых файлов, они могут храниться вместе с программным кодом в текстовом сегменте или иметь отдельный сегмент для инициализированных данных.
Определение деталей будет также зависеть от платформы - скорее всего, есть инструменты, которые могут подсказать вам, где она находится. Некоторые даже дадут вам контроль над такими деталями, если вы этого захотите (например, gnu ld позволяет вам предоставить скрипт, который расскажет все о том, как группировать данные, код и т. Д.)
К вашему сведению, просто резервное копирование других ответов:
Стандарт: ISO / IEC 14882: 2003 гласит:
2.13. Строковые литералы
[...] Обычный строковый литерал имеет тип "массив
n const char
"И статическая продолжительность хранения (3.7)Все ли строковые литералы различны (т.е. хранятся в неперекрывающихся объектах), определяется реализацией. Эффект попытки изменить строковый литерал не определен.
GCC делает .rodata
раздел, который отображается "где-то" в адресном пространстве и помечен только для чтения,
Visual C++ (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
Это зависит от формата вашего исполняемого файла. Можно подумать об этом, если бы вы программировали на ассемблере, вы могли бы поместить строковые литералы в сегмент данных вашей программы на ассемблере. Ваш компилятор C делает что-то подобное, но все зависит от того, для какой системы вы бинарный.
Строковые литералы часто выделяются только для чтения, что делает их неизменяемыми. Тем не менее, в некоторых компиляторах модификация возможна с помощью "умного трюка". А умный трюк заключается в "использовании указателя символа, указывающего на память"... помните, что некоторые компиляторы могут этого не допустить.. Вот демо
char *tabHeader = "Sound";
*tabHeader = 'L';
printf("%s\n",tabHeader); // Displays "Lound"
Поскольку это может отличаться от компилятора к компилятору, лучший способ - отфильтровать дамп объекта для искомого строкового литерала:
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....