Строковые литералы: куда они идут?

162

Меня интересует, где строковые литералы распределяются / хранятся.

Я нашел один интригующий ответ здесь , говоря:

Определение строки inline фактически встраивает данные в саму программу и не может быть изменено (некоторые компиляторы допускают это умным трюком, не беспокойтесь).

Но это было связано с C ++, не говоря уже о том, что он говорит не беспокоить.

Я беспокоюсь. = D

Итак, мой вопрос: где и как хранится мой строковый литерал? Почему я не должен пытаться изменить это? Реализация зависит от платформы? Кто-нибудь хочет уточнить «умный трюк»?

Крис Купер
источник

Ответы:

126

Общепринятым методом является помещение строковых литералов в раздел «данные только для чтения», который отображается в пространство процесса только для чтения (поэтому его нельзя изменить).

Это зависит от платформы. Например, более простые архитектуры микросхем могут не поддерживать сегменты памяти только для чтения, поэтому сегмент данных будет доступен для записи.

Вместо этого попробуйте найти способ сделать строковые литералы изменяемыми (это будет сильно зависеть от вашей платформы и со временем изменится), просто используйте массивы:

char foo[] = "...";

Компилятор организует инициализацию массива из литерала, и вы можете изменить массив.

R Самуэль Клатчко
источник
5
Да, я использую массивы, когда хочу иметь изменяемые строки. Мне было просто любопытно. Спасибо.
Крис Купер
2
Вы должны быть осторожны с переполнением буфера при использовании массивов для изменяемых строк, хотя простое написание строки длиннее длины массива (например, foo = "hello"в этом случае) может вызвать непреднамеренные побочные эффекты ... (при условии, что вы не выделения памяти с newили что - то)
ДЖОННИ
2
Есть ли при использовании массива строка идет в стек или в другом месте?
Сурадж Джейн
Разве мы не можем использовать char *p = "abc";для создания изменяемых строк, как это было сказано @ChrisCooper
KPMG
52

Там нет ни одного ответа на это. Стандарты C и C ++ просто говорят, что строковые литералы имеют статическую продолжительность хранения, любая попытка их изменения приводит к неопределенному поведению, а несколько строковых литералов с одинаковым содержимым могут или не могут совместно использовать одно и то же хранилище.

В зависимости от системы, для которой вы пишете, и возможностей используемого формата исполняемого файла, они могут храниться вместе с программным кодом в текстовом сегменте или иметь отдельный сегмент для инициализированных данных.

Определение деталей также будет зависеть от платформы - скорее всего, есть инструменты, которые могут подсказать вам, где она находится. Некоторые даже дадут вам контроль над такими деталями, если вы этого хотите (например, gnu ld позволяет вам предоставить скрипт, который расскажет все о том, как группировать данные, код и т. Д.)

Джерри Гроб
источник
1
Я считаю маловероятным, что строковые данные будут храниться непосредственно в сегменте .text. Для очень коротких литералов, я мог видеть , генерирующего код компилятора , например , как movb $65, 8(%esp); movb $66, 9(%esp); movb $0, 10(%esp)для строки "AB", но подавляющее большинство времени, он будет находиться в сегменте не-кода , такие как .dataили .rodataили т.п. ( в зависимости от наличия или отсутствия целевых опор сегменты только для чтения).
Адам Розенфилд,
Если строковые литералы действительны в течение всей продолжительности программы, даже во время уничтожения статических объектов, тогда допустимо ли возвращать константную ссылку на строковый литерал? Почему эта программа показывает ошибку во время выполнения, см. Ideone.com/FTs1Ig
Destructor
@AdamRosenfield: Если вам скучно, вы можете посмотреть (для одного примера) устаревший формат UNIX a.out (например, freebsd.org/cgi/… ). Вы должны быстро заметить, что он поддерживает только один сегмент данных, который всегда доступен для записи. Поэтому, если вам нужны строковые литералы только для чтения, по сути, единственное место, куда они могут перейти, - это текстовый сегмент (и да, в то время, когда компоновщики часто делали именно это).
Джерри Гроб
48

Почему я не должен пытаться изменить это?

Потому что это неопределенное поведение. Цитата из проекта 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[]: stack
  • 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), и мы, конечно, можем его изменить.

Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
22

К вашему сведению, просто резервное копирование других ответов:

Стандарт: ISO / IEC 14882: 2003 гласит:

2.13. Строковые литералы

  1. [...] Обычный строковый литерал имеет тип «массив n const char» и статическую длительность хранения (3.7)

  2. Все ли строковые литералы различны (т.е. хранятся в неперекрывающихся объектах), определяется реализацией. Эффект попытки изменить строковый литерал не определен.

Justicle
источник
2
Полезная информация, но ссылка на уведомление предназначена для C ++, тогда как вопрос касается c
Grijesh Chauhan
1
подтвердил № 2 в 2.13. С опцией -Os (оптимизировать по размеру) gcc перекрывает строковые литералы в .rodata.
Пэн Чжан
14

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
Алекс Будовски
источник
1
Я не вижу, как получить разборку раздела rdata с помощью objdump.
user2284570 30.11.15
@ user2284570, потому что этот раздел не содержит сборки. Содержит данные.
Алекс Будовски
1
Просто вопрос, чтобы получить более читаемый вывод. Я имею в виду, что я хотел бы получить строки с разборкой вместо адреса к этим разделам. (Подол вы знаете, printf("some null terminated static string");а не printf(*address);в C)
user2284570
4

Это зависит от формата вашего исполняемого файла . Можно подумать об этом, если бы вы программировали на ассемблере, вы могли бы поместить строковые литералы в сегмент данных вашей программы на ассемблере. Ваш компилятор C делает что-то подобное, но все зависит от того, для какой системы вы бинарный.

Parappa
источник
2

Строковые литералы часто размещаются в постоянной памяти, что делает их неизменяемыми. Тем не менее, в некоторых компиляторах модификация возможна с помощью «умного трюка». А умный трюк заключается в «использовании указателя символа, указывающего на память» ... помните, что некоторые компиляторы могут этого не допустить .. Вот демо

char *tabHeader = "Sound";
*tabHeader = 'L';
printf("%s\n",tabHeader); // Displays "Lound"
Сахил Джайн
источник
0

Поскольку это может отличаться от компилятора к компилятору, лучший способ - отфильтровать дамп объекта для искомого строкового литерала:

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....
Mihai
источник