Строкові літерали: куди вони йдуть?


161

Мене цікавить, де виділяються / зберігаються рядкові літерали.

Я знайшов один інтригуючий відповідь тут , кажучи:

Визначення рядкового рядка фактично вбудовує дані в саму програму і не може бути змінено (деякі компілятори дозволяють це розумним трюком, не турбуйте).

Але це стосувалося C ++, не кажучи вже про те, що він говорить не турбувати.

Мене турбує. = D

Отже, моє запитання: де і як зберігається моя лінійна літера? Чому я не намагаюся це змінити? Чи відрізняється реалізація залежно від платформи? Хтось не хоче деталізувати "розумний трюк"?

Відповіді:


125

Загальна техніка полягає в тому, щоб рядкові літерали були розміщені в розділі "лише для читання даних", який відображається в просторі процесу як лише для читання (тому ви не можете його змінити).

Це залежить від платформи. Наприклад, більш прості архітектури мікросхем можуть не підтримувати сегменти пам'яті лише для читання, тому сегмент даних буде доступним для запису.

Замість цього спробуйте винайти трюк, щоб зробити літеральні рядки змінними (це буде сильно залежати від вашої платформи і може змінитися з часом), просто використовуйте масиви:

char foo[] = "...";

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


5
Так, я використовую масиви, коли хочу мати змінні рядки. Мені було просто цікаво. Дякую.
Кріс Купер

2
Вам потрібно бути обережним щодо переповнення буфера при використанні масивів для змінних рядків, однак - просто запис рядка довше довжини масиву (наприклад, foo = "hello"у цьому випадку) може спричинити небажані побічні ефекти ... (припустимо, що ви не повторно виділення пам’яті з newчи чимсь)
johnny

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 подібний (залежно від того, підтримує цільова чи ні) сегменти лише для читання).
Адам Розенфілд

Якщо рядкові літерали дійсні протягом усієї тривалості програми, навіть під час знищення статичних об'єктів, чи дійсно повернути const посилання на рядковий літерал? Чому ця програма показує помилку виконання, дивіться 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";

визначає "прості" об'єкти масиву char sта tелементи яких ініціалізуються літеральними рядками символів.

Ця декларація ідентична

char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };

Вміст масивів може змінюватися. З іншого боку, декларація

char *p = "abc";

визначає pз типом "покажчик на char" та ініціалізує його для вказівки на об'єкт типу "масив char" довжиною 4, елементи якого ініціалізовані літеральним рядком символів. Якщо буде здійснена спроба pзмінити вміст масиву, поведінка не визначена.

Куди вони йдуть?

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

Це означає, що скрипт посилання за замовчуванням скидає .textі .rodataв сегмент, який можна виконати, але не змінити ( Flags = R E). Спроба змінити такий сегмент призводить до сегментації в Linux.

Якщо ми робимо те саме для char[]:

 char s[] = "abc";

ми отримуємо:

17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)

тому він зберігається в стеку (щодо %rbp), і ми, звичайно, можемо його змінити.


22

FYI, просто резервне копіювання інших відповідей:

Стандарт: ISO / IEC 14882: 2003 говорить:

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

  1. [...] Звичайний літеральний рядок має тип "масив n const char" та статичну тривалість зберігання (3.7)

  2. Незалежно від того, чи всі літеральні рядки відрізняються (тобто зберігаються в об'єктах, що не збігаються), визначено реалізацією. Ефект спроби змінити літеральний рядок не визначений.


2
Корисна інформація, але посилання на повідомлення є для C ++, тоді як питання позначено c
Grijesh Chauhan

1
підтверджено №2 в 2.13. З опцією -Os (оптимізуйте за розміром), gcc перекриває рядкові літерали у .rodata.
Пен Чжан

14

gcc створює .rodataрозділ, який відображається "десь" в адресному просторі і позначається лише для читання,

Visual C ++ ( cl.exe) робить a.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.
користувач2284570

@ user2284570, це тому, що цей розділ не містить збірки. Він містить дані.
Олексій Будовський

1
Справа лише в тому, щоб отримати більш читабельний вихід. Я маю на увазі, що я хотів би, щоб рядки були накреслені розбиранням замість адреси до цих розділів. (Гем ви знаєте printf("some null terminated static string");замість printf(*address);C)
користувач2284570

4

Це залежить від формату вашого виконуваного файлу . Один із способів задуматися над тим, що якби ви програмували складання, ви можете помістити рядкові літерали в сегмент даних вашої програми складання. Ваш компілятор C робить щось подібне, але все залежить від того, для якої системи ви складаєте двійковий код.


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....
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.