Por que os literais de string C são somente leitura?

29

Que vantagem (s) dos literais de cadeia de caracteres sendo somente leitura justificam (-ies / -ied):

  1. Mais uma maneira de dar um tiro no próprio pé

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
  2. Incapacidade de inicializar com elegância uma matriz de leitura e gravação de palavras em uma linha:

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
  3. Complicando o próprio idioma.

    char *foo = "bar";
    char var[] = "baz";
    some_func(foo); /* VERY DANGEROUS! */
    some_func(var); /* LESS DANGEROUS! */

Economizando memória? Eu li em algum lugar (não consegui encontrar a fonte agora) que, há muito tempo, quando a RAM era escassa, os compiladores tentavam otimizar o uso da memória, mesclando seqüências semelhantes.

Por exemplo, "mais" e "regex" se tornariam "moregex". Isso ainda é verdade hoje, na era dos filmes digitais com qualidade blu-ray? Entendo que os sistemas embarcados ainda operam em um ambiente de recursos restritos, mas a quantidade de memória disponível aumentou dramaticamente.

Problemas de compatibilidade? Presumo que um programa legado que tentasse acessar a memória somente leitura travasse ou continuasse com um bug não descoberto. Portanto, nenhum programa legado deve tentar acessar literal de string e, portanto, permitir a gravação em literal de string não prejudicaria programas legados portáteis válidos, não hackeados .

Existem outras razões? Meu raciocínio está incorreto? Seria razoável considerar uma alteração nos literais das cadeias de leitura e gravação nos novos padrões C ou pelo menos adicionar uma opção ao compilador? Isso foi considerado antes ou meus "problemas" são muito pequenos e insignificantes para incomodar alguém?

Marius Macijauskas
fonte
12
Suponho que você tenha examinado a aparência de literais de string no código compilado ?
2
Veja a montagem que o link que eu forneci contém. É logo ali.
8
Seu exemplo "moregex" não funcionaria por causa do encerramento nulo.
precisa saber é
4
Você não deseja escrever sobre constantes, porque isso mudará seu valor. A próxima vez que você quiser usar a mesma constante, seria diferente. O compilador / tempo de execução precisa originar as constantes de algum lugar e, onde quer que esteja, você não deve modificar.
Erik Eidt
1
'Portanto, literais de string são armazenados na memória do programa, não na RAM, e o excesso de buffer resultaria na corrupção do próprio programa?' A imagem do programa também está na RAM. Para ser mais preciso, os literais das strings são armazenados no mesmo segmento de RAM usado para armazenar a imagem do programa. E sim, a substituição da string pode corromper o programa. Nos dias do MS-DOS e do CP / M, não havia proteção de memória; era possível fazer coisas assim, e isso geralmente causava problemas terríveis. Os primeiros vírus de PC usavam truques como esse para modificar seu programa, formatando seu disco rígido quando você tentava executá-lo.
Charles E. Grant

Respostas:

40

Historicamente (talvez reescrevendo partes dela), era o contrário. Nos primeiros computadores do início dos anos 1970 (talvez PDP-11 ) executando um C embrionário prototípico (talvez BCPL ), não havia MMU e nenhuma proteção de memória (que existia na maioria dos mainframes IBM / 360 mais antigos ). Portanto, cada byte de memória (incluindo aqueles que manipulam cadeias literais ou código de máquina) pode ser sobrescrito por um programa incorreto (imagine um programa mudando alguns %para /uma cadeia de formato printf (3) ). Portanto, cadeias literais e constantes eram graváveis.

Quando adolescente, em 1975, eu codifiquei no museu Palais de la Découverte em Paris em computadores antigos da década de 1960 sem proteção de memória: o IBM / 1620 tinha apenas uma memória principal - que era possível inicializar através do teclado, e era preciso digitar várias dezenas de dígitos para ler o programa inicial em fitas perfuradas; CAB / 500 tinha uma memória de tambor magnético; você pode desativar a gravação de algumas faixas através de interruptores mecânicos próximos ao tambor.

Mais tarde, os computadores obtiveram algum tipo de unidade de gerenciamento de memória (MMU) com alguma proteção de memória. Havia um dispositivo que proibia a CPU de substituir algum tipo de memória. Portanto, alguns segmentos de memória, principalmente o segmento de código (também conhecido como .textsegmento), tornaram-se somente leitura (exceto pelo sistema operacional que os carregou do disco). Era natural que o compilador e o vinculador colocassem as cadeias literais nesse segmento de código, e as cadeias literais se tornassem somente leitura. Quando seu programa tentou substituí-los, foi um comportamento indefinido . E ter um segmento de código somente leitura na memória virtual oferece uma vantagem significativa: vários processos executando o mesmo programa compartilham a mesma RAM ( memória físicapáginas) para esse segmento de código (consulte o MAP_SHAREDsinalizador para mmap (2) no Linux).

Hoje, os microcontroladores baratos têm alguma memória somente leitura (por exemplo, Flash ou ROM) e mantêm seu código (e as strings literais e outras constantes) lá. E microprocessadores reais (como o do seu tablet, laptop ou desktop) possuem uma sofisticada unidade de gerenciamento de memória e máquinas de cache usadas para memória virtual e paginação . Portanto, o segmento de código do programa executável (por exemplo, no ELF ) é mapeado como um segmento somente leitura, compartilhável e executável (por mmap (2) ou execve (2) no Linux; BTW, você pode fornecer diretrizes para ldpara obter um segmento de código gravável, se você realmente quisesse). Escrever ou abusar geralmente é uma falha de segmentação .

Portanto, o padrão C é barroco: legalmente (apenas por razões históricas), cadeias literais não são const char[]matrizes, mas somente char[]matrizes que são proibidas de serem substituídas.

BTW, poucas linguagens atuais permitem que os literais de strings sejam substituídos (mesmo o Ocaml, que historicamente - e mal - possuía strings literais graváveis, mudou esse comportamento recentemente em 4.02, e agora possui strings somente leitura).

Os compiladores C atuais podem otimizar, compartilhar "ions"e "expressions"compartilhar seus últimos 5 bytes (incluindo o byte nulo final).

Tente compilar o código C em arquivo foo.ccom gcc -O -fverbose-asm -S foo.ce olhar dentro do arquivo assembler gerado foo.spelo GCC

Por fim, a semântica de C é complexa o suficiente (leia mais sobre o CompCert e o Frama-C, que estão tentando capturá-lo) e a adição de seqüências literais constantes e graváveis ​​tornaria ainda mais misteriosa, tornando os programas mais fracos e menos seguros (e com menos comportamento definido), portanto, é muito improvável que os futuros padrões C aceitem cadeias literais graváveis. Talvez, pelo contrário, eles os fizessem const char[]matrizes como deveriam ser moralmente.

Observe também que, por muitas razões, os dados mutáveis ​​são mais difíceis de manipular pelo computador (coerência do cache), codificar, entender pelo desenvolvedor, do que dados constantes. Portanto, é preferível que a maioria dos seus dados (e principalmente as cadeias literais) permaneçam imutáveis . Leia mais sobre o paradigma de programação funcional .

Nos velhos Fortran77 dias na IBM / 7094, um erro poderia até mesmo mudar uma constante: se você CALL FOO(1)e se FOOaconteceu para modificar seu argumento passado por referência a 2, a implementação pode ter alterado outras ocorrências de 1 para 2, e que foi um realmente bug impertinente, bastante difícil de encontrar.

Basile Starynkevitch
fonte
Isso serve para proteger as strings como constantes? Mesmo que eles não estejam definidos como constpadrão ( stackoverflow.com/questions/2245664/… )?
Marius Macijauskas
Você tem certeza de que os primeiros computadores não tinham memória somente leitura? Isso não era consideravelmente mais barato que o carneiro? Além disso, colocá-los na memória RO não faz com que o UB tente modificá-los erroneamente, mas confie no OP não fazendo isso e ele violando essa confiança. Veja, por exemplo, Fortran-programas onde todos os literais 1s repente se comportam como 2s e tão divertido ...
Deduplicator
1
Quando adolescente em um museu, codifiquei em 1975 os antigos computadores IBM / 1620 e CAB500. Nem tinha qualquer ROM: IBM / 1620 tinha memória de núcleo, e CAB500 tinha um tambor magnético (algumas faixas poderia ser desativado para ser escrito por um interruptor mecânico)
Basile Starynkevitch
2
Também vale ressaltar: Colocar literais no segmento de código significa que eles podem ser compartilhados entre várias cópias do programa, porque a inicialização ocorre no tempo de compilação, em vez do tempo de execução.
Blrfl 27/08/15
@Duplicator Bem, eu vi uma máquina executando uma variante BASIC que permitiu alterar constantes inteiras (não tenho certeza se você precisava enganá-lo para fazê-lo, por exemplo, passando argumentos "byref" ou se um simples let 2 = 3funcionava). Isso resultou em muita diversão (na definição da palavra Dwarf Fortress), é claro. Não tenho idéia de como o intérprete foi projetado para permitir isso, mas foi.
Luaan 4/09/15
2

Os compiladores não puderam combinar "more"e "regex", porque o primeiro possui um byte nulo após o esegundo x, mas muitos compiladores combinavam literais de strings que correspondiam perfeitamente, e alguns também combinavam literais de strings que compartilhavam uma cauda comum. O código que altera um literal de cadeia de caracteres pode, portanto, alterar um literal de cadeia de caracteres diferente, usado para algum propósito totalmente diferente, mas que contém os mesmos caracteres.

Uma questão semelhante surgiria no FORTRAN antes da invenção de C. Os argumentos sempre eram passados ​​por endereço e não por valor. Uma rotina para adicionar dois números seria assim equivalente a:

float sum(float *f1, float *f2) { return *f1 + *f2; }

No caso de alguém querer passar um valor constante (por exemplo, 4.0) para sum, o compilador criaria uma variável anônima e a inicializaria 4.0. Se o mesmo valor fosse passado para várias funções, o compilador passaria o mesmo endereço para todas elas. Como conseqüência, se uma função que modificou um de seus parâmetros recebeu uma constante de ponto flutuante, o valor dessa constante em outro local do programa pode ser alterado como resultado, levando ao ditado "Variáveis ​​não; constantes não são 't ".

supercat
fonte