Literały ciągów: gdzie one idą?

161

Interesuje mnie, gdzie są przydzielane / przechowywane literały ciągów.

Znalazłem tutaj jedną intrygującą odpowiedź , mówiącą:

Zdefiniowanie ciągu w linii faktycznie osadza dane w samym programie i nie można go zmienić (niektóre kompilatory pozwalają na to sprytną sztuczką, nie przejmuj się).

Ale miało to coś wspólnego z C ++, nie wspominając o tym, że mówi, żeby nie zawracać sobie głowy.

Przeszkadzam. = D

Więc moje pytanie brzmi: gdzie i jak jest przechowywany mój dosłowny ciąg? Dlaczego nie powinienem próbować tego zmieniać? Czy implementacja różni się w zależności od platformy? Czy ktoś chce rozwinąć „sprytną sztuczkę”?

Chris Cooper
źródło

Odpowiedzi:

125

Powszechną techniką jest umieszczanie literałów łańcuchowych w sekcji „tylko do odczytu”, która jest odwzorowywana w przestrzeni procesu jako tylko do odczytu (dlatego nie można tego zmienić).

Różni się w zależności od platformy. Na przykład prostsze architektury chipów mogą nie obsługiwać segmentów pamięci tylko do odczytu, więc segment danych będzie zapisywalny.

Zamiast tego spróbuj wymyślić sztuczkę, aby zmienić literały ciągów (będzie to w dużym stopniu zależne od Twojej platformy i może się zmieniać w czasie), po prostu użyj tablic:

char foo[] = "...";

Kompilator zorganizuje inicjalizację tablicy z literału i możesz zmodyfikować tablicę.

R Samuel Klatchko
źródło
5
Tak, używam tablic, gdy chcę mieć zmienne ciągi. Byłem tylko ciekawy. Dzięki.
Chris Cooper
2
Musisz jednak uważać na przepełnienie bufora, gdy używasz tablic dla zmiennych ciągów, jednak - po prostu napisanie ciągu dłuższego niż długość tablicy (np. foo = "hello"W tym przypadku) może spowodować niezamierzone efekty uboczne ... (zakładając, że nie alokowanie pamięci z newczy czymś)
johnny
2
Czy użycie ciągu tablicowego trafia na stos lub gdzie indziej?
Suraj Jain
Czy nie możemy użyć char *p = "abc";do tworzenia zmiennych ciągów, jak inaczej powiedział @ChrisCooper
KPMG
52

Nie ma na to jednej odpowiedzi. Standardy C i C ++ mówią po prostu, że literały ciągów mają statyczny czas trwania, każda próba ich modyfikacji daje niezdefiniowane zachowanie, a wiele literałów ciągów o tej samej zawartości może, ale nie musi, współużytkować tę samą pamięć.

W zależności od systemu, dla którego piszesz, i możliwości formatu pliku wykonywalnego, którego używa, mogą być one przechowywane wraz z kodem programu w segmencie tekstowym lub mogą mieć oddzielny segment dla zainicjowanych danych.

Określenie szczegółów będzie się różnić w zależności od platformy - najprawdopodobniej obejmują narzędzia, które mogą powiedzieć, gdzie je umieszcza. Niektóre nawet dadzą ci kontrolę nad takimi szczegółami, jeśli chcesz (np. Gnu ld pozwala ci dostarczyć skrypt, który powie wszystko o tym, jak grupować dane, kod itp.)

Jerry Coffin
źródło
1
Uważam, że jest mało prawdopodobne, aby dane ciągów były przechowywane bezpośrednio w segmencie .text. Dla bardzo krótkich literały, mogłem zobaczyć kompilatora kodu generowania takich jak movb $65, 8(%esp); movb $66, 9(%esp); movb $0, 10(%esp)na łańcuchu "AB", ale większość czasu, to będzie w segmencie non-kodu, takich jak .datalub .rodataczy podobnego (w zależności od tego, czy podpór docelowych segmenty tylko do odczytu).
Adam Rosenfield
Jeśli literały łańcuchowe są ważne przez cały czas trwania programu, nawet podczas niszczenia obiektów statycznych, to czy prawidłowe jest zwrócenie odwołania const do literału ciągu? Dlaczego ten program wyświetla błąd w czasie wykonywania, patrz ideone.com/FTs1Ig
Destructor
@AdamRosenfield: Jeśli czasem się nudzisz, możesz przyjrzeć się (na przykład) starszemu formatowi UNIX a.out (np. Freebsd.org/cgi/… ). Należy szybko zauważyć, że obsługuje tylko jeden segment danych, który jest zawsze zapisywalny. Więc jeśli chcesz, aby literały ciągów tylko do odczytu, zasadniczo jedynym miejscem, do którego mogą się udać, jest segment tekstu (i tak, w tamtym czasie konsolidatory często to robiły).
Jerry Coffin
48

Dlaczego nie powinienem próbować tego zmieniać?

Ponieważ jest to niezdefiniowane zachowanie. Cytat z C99 N1256, szkic 6.7.8 / 32 "Inicjalizacja" :

PRZYKŁAD 8: Deklaracja

char s[] = "abc", t[3] = "abc";

określa „zwykły” obiektów tablicy char si tktórego elementy są inicjalizowane napisowych charakter.

Ta deklaracja jest identyczna z

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

Zawartość tablic można modyfikować. Z drugiej strony deklaracja

char *p = "abc";

definiuje ptypem „wskaźnik do znaku” i inicjalizuje go, aby wskazywał na obiekt typu „tablica znaków” o długości 4, którego elementy są inicjalizowane literałem będącym ciągiem znaków. Jeśli zostanie podjęta próba pzmodyfikowania zawartości tablicy, zachowanie jest niezdefiniowane.

Dokąd oni poszli?

GCC 4.8 x86-64 ELF Ubuntu 14.04:

  • char s[]: stos
  • char *s:
    • .rodata sekcja pliku obiektowego
    • ten sam segment, w którym .textjest zrzucana sekcja pliku obiektowego, która ma uprawnienia do odczytu i wykonywania, ale nie ma uprawnień do zapisu

Program:

#include <stdio.h>

int main() {
    char *s = "abc";
    printf("%s\n", s);
    return 0;
}

Kompiluj i dekompiluj:

gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o

Wyjście zawiera:

 char *s = "abc";
8:  48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
f:  00 
        c: R_X86_64_32S .rodata

Więc ciąg jest przechowywany w .rodatasekcji.

Następnie:

readelf -l a.out

Zawiera (uproszczone):

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

Oznacza to, że domyślny skrypt konsolidujący zrzuca zarówno .texti .rodatado segmentu, który można wykonać, ale nie można go modyfikować ( Flags = R E). Próba zmodyfikowania takiego segmentu prowadzi do segfaulta w Linuksie.

Jeśli zrobimy to samo dla char[]:

 char s[] = "abc";

otrzymujemy:

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

więc zostaje zapisany na stosie (względem %rbp) i oczywiście możemy go zmodyfikować.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
źródło
22

FYI, po prostu zrób kopię zapasową innych odpowiedzi:

Norma: ISO / IEC 14882: 2003 mówi:

2.13. Literały ciągów

  1. [...] Zwykły literał ciągu ma typ „tablica n const char” i statyczny czas trwania (3.7)

  2. To, czy wszystkie literały łańcuchowe są różne (to znaczy, że są przechowywane w nienakładających się obiektach), jest zdefiniowane w implementacji. Efekt próby zmodyfikowania literału ciągu jest niezdefiniowany.

Justicle
źródło
2
Przydatne informacje, ale zawiadomienie link jest dla C ++, natomiast pytanie tanged do c
Grijesh Chauhan
1
potwierdzone # 2 w 2.13. Z opcją -Os (optymalizacja dla rozmiaru), gcc nakłada się na literały łańcuchowe w .rodata.
Peng Zhang,
14

gcc tworzy .rodatasekcję, która jest mapowana „gdzieś” w przestrzeni adresowej i jest oznaczona jako tylko do odczytu,

Visual C ++ ( cl.exe) tworzy .rdatasekcję w tym samym celu.

Możesz spojrzeć na dane wyjściowe z dumpbinlub objdump(w systemie Linux), aby zobaczyć sekcje swojego pliku wykonywalnego.

Na przykład

>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
Alex Budovski
źródło
1
Nie widzę, jak uzyskać demontaż sekcji rdata za pomocą objdump.
user2284570
@ user2284570, to dlatego, że ta sekcja nie zawiera asemblera. Zawiera dane.
Alex Budovski
1
Tylko kwestia uzyskania bardziej czytelnych wyników. Chodzi mi o to, że chciałbym uzyskać ciągi wbudowane z demontażem zamiast adresu do tych sekcji. (hem, który znasz printf("some null terminated static string");zamiast printf(*address);w C)
user2284570
4

To zależy od formatu twojego pliku wykonywalnego . Jednym ze sposobów myślenia o tym jest to, że jeśli zajmujesz się programowaniem w asemblerze, możesz umieścić literały łańcuchowe w segmencie danych swojego programu asemblerowego. Twój kompilator C robi coś takiego, ale wszystko zależy od tego, dla jakiego systemu tworzony jest plik binarny.

Parappa
źródło
2

Literały ciągów są często przydzielane do pamięci tylko do odczytu, dzięki czemu są niezmienne. Jednak w niektórych kompilatorach modyfikacja jest możliwa dzięki "sprytnej sztuczce" .. A sprytna sztuczka polega na "użyciu wskaźnika znakowego wskazującego na pamięć" ... pamiętaj, że niektóre kompilatory mogą na to nie pozwalać ... Oto demo

char *tabHeader = "Sound";
*tabHeader = 'L';
printf("%s\n",tabHeader); // Displays "Lound"
Sahil Jain
źródło
0

Ponieważ może się to różnić w zależności od kompilatora, najlepszym sposobem jest przefiltrowanie zrzutu obiektu dla wyszukanego literału ciągu:

objdump -s main.o | grep -B 1 str

gdzie -swymusza objdumpwyświetlenie pełnej zawartości wszystkich sekcji, main.ojest plikiem obiektowym, -B 1wymusza greprównież wypisanie jednej linii przed dopasowaniem (abyś mógł zobaczyć nazwę sekcji) i strjest literałem ciągu, którego szukasz.

Z gcc na komputerze z systemem Windows i jedną zmienną zadeklarowaną w mainlike

char *c = "whatever";

bieganie

objdump -s main.o | grep -B 1 whatever

zwroty

Contents of section .rdata:
 0000 77686174 65766572 00000000           whatever....
mihai
źródło