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

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

Я нашел один интригующий ответ 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....