Imprimir ponteiros nulos com% p é um comportamento indefinido?

93

É um comportamento indefinido imprimir ponteiros nulos com o %pespecificador de conversão?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

A questão se aplica ao padrão C, e não às implementações C.

Dror K.
fonte
A realmente não acho que alguém (incluindo o comitê C) se preocupa muito com isso. É um problema bastante artificial, sem (ou quase nenhum) significado prático.
P__J__ de
é porque printf só exibe o valor e não toca (no sentido de ler ou escrever o objeto apontado) - não pode ser UB i ponteiro tem um valor de tipo válido (NULL é o valor válido )
P__J__
3
@PeterJ vamos dizer que o que você está dizendo é verdade (embora claramente o padrão indique o contrário), o fato sozinho, de que estamos debatendo sobre isso, torna a questão válida e correta, pois parece que a parte citada abaixo do padrão torna é muito difícil entender para um desenvolvedor regular o que diabos está acontecendo .. Quer dizer: a questão não merece o voto negativo, porque esse problema precisa de esclarecimento!
Peter Varo
2
@PeterJ essa é uma história diferente, obrigado pelo esclarecimento :)
Peter Varo

Respostas:

93

Este é um daqueles casos estranhos em que estamos sujeitos às limitações do idioma inglês e à estrutura inconsistente do padrão. Portanto, na melhor das hipóteses, posso apresentar um contra-argumento convincente, pois é impossível prová- lo :) 1


O código em questão exibe um comportamento bem definido.

Como [7.1.4] é a base da pergunta, vamos começar por aí:

Cada uma das seguintes instruções se aplica, a menos que seja explicitamente declarado de outra forma nas descrições detalhadas a seguir: Se um argumento para uma função tiver um valor inválido ( como um valor fora do domínio da função ou um ponteiro fora do espaço de endereço do programa, ou um ponteiro nulo , [... outros exemplos ...] ) [...] o comportamento é indefinido. [... outras afirmações ...]

Esta é uma linguagem desajeitada. Uma interpretação é que os itens na lista são UB para todas as funções da biblioteca, a menos que sejam substituídos pelas descrições individuais. Mas a lista começa com "como", indicando que é ilustrativa, não exaustiva. Por exemplo, não menciona a terminação nula correta de strings (crítica para o comportamento de eg strcpy).

Portanto, está claro que a intenção / escopo de 7.1.4 é simplesmente que um "valor inválido" leva ao UB (a menos que indicado de outra forma ). Temos que olhar para a descrição de cada função para determinar o que conta como um "valor inválido".

Exemplo 1 - strcpy

[7.21.2.3] diz apenas o seguinte:

A strcpyfunção copia a string apontada por s2(incluindo o caractere nulo de terminação) para a matriz apontada por s1. Se a cópia ocorrer entre objetos que se sobrepõem, o comportamento é indefinido.

Ele não faz nenhuma menção explícita de ponteiros nulos, mas também não faz menção de terminadores nulos. Em vez disso, infere-se de "string apontado por s2" que os únicos valores válidos são strings (ou seja, ponteiros para matrizes de caracteres terminados em nulo).

Na verdade, esse padrão pode ser visto em todas as descrições individuais. Alguns outros exemplos:

  • [7.6.4.1 (fenv)] armazena o ambiente de ponto flutuante atual no objeto apontado porenvp

  • [7.12.6.4 (frexp)] armazenar o inteiro no objeto int apontado porexp

  • [7.19.5.1 (fclose)] o fluxo apontado porstream

Exemplo 2 - printf

[7.19.6.1] diz isso sobre %p:

p- O argumento deve ser um indicador para void. O valor do ponteiro é convertido em uma seqüência de caracteres de impressão, de uma maneira definida pela implementação.

Nulo é um valor de ponteiro válido e esta seção não faz menção explícita de que nulo é um caso especial, nem que o ponteiro deve apontar para um objeto. Assim, é um comportamento definido.


1. A menos que um autor de padrões apareça, ou a menos que possamos encontrar algo semelhante a um documento lógico que esclareça as coisas.

Oliver Charlesworth
fonte
Os comentários não são para discussão extensa; esta conversa foi movida para o chat .
Bhargav Rao
1
"mas não faz menção a terminadores nulos" é fraco no Exemplo 1 - strcpy, pois a especificação diz "copia a string ". string é explicitamente definida como tendo um caractere nulo .
chux - Reintegrar Monica
1
@chux - Esse é o meu ponto - é preciso inferir o que é válido / inválido a partir do contexto, em vez de assumir que a lista em 7.1.4 é exaustiva. (No entanto, a existência desta parte da minha resposta fez um pouco mais sentido no contexto de comentários que já foram excluídos, argumentando que strcpy era um contra-exemplo.)
Oliver Charlesworth
1
O cerne da questão é como o leitor interpretará tal como . Isso significa que alguns exemplos de possíveis valores inválidos são ? Isso significa que alguns exemplos que são sempre valores inválidos são ? Para que fique registado, vou com a primeira interpretação.
ninjalj
1
@ninjalj - Sim, concordo. Isso é essencialmente o que estou tentando transmitir em minha resposta aqui, ou seja, "estes são exemplos de tipos de coisas que podem ser valores inválidos". :)
Oliver Charlesworth
20

A resposta curta

Sim . A impressão de ponteiros nulos com o %pespecificador de conversão tem comportamento indefinido. Dito isso, não tenho conhecimento de qualquer implementação em conformidade existente que se comportaria mal.

A resposta se aplica a qualquer um dos padrões C (C89 / C99 / C11).


A resposta longa

O %pespecificador de conversão espera um argumento do tipo ponteiro para void, a conversão do ponteiro em caracteres imprimíveis é definida pela implementação. Ele não afirma que um ponteiro nulo é esperado.

A introdução às funções de biblioteca padrão afirma que ponteiros nulos como argumentos para funções (biblioteca padrão) são considerados valores inválidos, a menos que seja explicitamente declarado de outra forma.

C99 / C11 §7.1.4 p1

[...] Se um argumento para uma função tiver um valor inválido (como [...] um ponteiro nulo, [...] o comportamento é indefinido.

Exemplos de funções (biblioteca padrão) que esperam ponteiros nulos como argumentos válidos:

  • fflush() usa um ponteiro nulo para liberar "todos os fluxos" (que se aplicam).
  • freopen() usa um ponteiro nulo para indicar o arquivo "atualmente associado" ao fluxo.
  • snprintf() permite passar um ponteiro nulo quando 'n' é zero.
  • realloc() usa um ponteiro nulo para alocar um novo objeto.
  • free() permite passar um ponteiro nulo.
  • strtok() usa um ponteiro nulo para chamadas subsequentes.

Se considerarmos o caso snprintf(), faz sentido permitir a passagem de um ponteiro nulo quando 'n' é zero, mas este não é o caso para outras funções (biblioteca padrão) que permitem um zero 'n' semelhante. Por exemplo: memcpy(), memmove(), strncpy(), memset(),memcmp() .

Não é especificado apenas na introdução à biblioteca padrão, mas também mais uma vez na introdução a estas funções:

C99 §7.21.1 p2 / C11 §7.24.1 p2

Onde um argumento declarado como size_tn especifica o comprimento da matriz para uma função, n pode ter o valor zero em uma chamada para essa função. A menos que explicitamente declarado de outra forma na descrição de uma função particular nesta subseção, os argumentos de ponteiro em tal chamada ainda devem ter valores válidos conforme descrito em 7.1.4.


É intencional?

Eu não sei se o UB de %pcom um ponteiro nulo é de fato intencional, mas como o padrão afirma explicitamente que os ponteiros nulos são considerados valores inválidos como argumentos para funções de biblioteca padrão, ele vai e especifica explicitamente os casos em que um nulo ponteiro é um argumento válido (snprintf, livre, etc), e então ele vai e mais uma vez repete a exigência para os argumentos para ser válido, mesmo em casos de zeros 'n' ( memcpy, memmove, memset), então eu acho que é razoável supor que o O comitê de padrões C não está muito preocupado em ter essas coisas indefinidas.

Dror K.
fonte
Os comentários não são para discussão extensa; esta conversa foi movida para o chat .
Bhargav Rao
1
@JeroenMostert: Qual é a intenção desse argumento? A citação de 7.1.4 fornecida é bastante clara, não é? O que há para discutir "a menos que seja explicitamente declarado o contrário" quando não está sendo declarado de outra forma? O que há para argumentar sobre o fato de que a biblioteca de funções de string (não relacionadas) tem um texto semelhante, de modo que o texto não parece ser acidental? Acho que essa resposta (embora não seja realmente útil na prática ) é a mais correta possível.
Damon
3
@Damon: Seu hardware mítico não é mítico, existem muitas arquiteturas onde valores que não representam endereços válidos podem não ser carregados em registradores de endereços. Porém, passar ponteiros nulos como argumentos de função ainda é necessário para funcionar nessas plataformas como um mecanismo geral. Apenas colocar um na pilha não vai explodir as coisas.
Jeroen Mostert
1
@anatolyg: Em processadores x86, os endereços têm duas partes - um segmento e um deslocamento. No 8086, carregar um registrador de segmento é como carregar qualquer outro, mas em todas as máquinas posteriores, ele busca um descritor de segmento. Carregar um descritor inválido causa uma armadilha. Um monte de código para 80386 e processadores posteriores, no entanto, usa apenas um segmento, e assim nunca carrega registradores de segmento em tudo .
supercat de
1
Acho que todos concordariam que imprimir um ponteiro nulo com %pnão deveria ser um comportamento indefinido
MM
-1

Os autores do Padrão C não fizeram nenhum esforço para listar exaustivamente todos os requisitos comportamentais que uma implementação deve atender para ser adequada a qualquer propósito específico. Em vez disso, eles esperavam que as pessoas que escreviam compiladores exercessem uma certa dose de bom senso, quer o padrão exigisse ou não.

A questão de saber se algo invoca UB raramente é útil por si só. As verdadeiras questões importantes são:

  1. Alguém que está tentando escrever um compilador de qualidade deve fazer com que ele se comporte de maneira previsível? Para o cenário descrito, a resposta é claramente sim.

  2. Os programadores devem ter o direito de esperar que compiladores de qualidade para qualquer coisa semelhante a plataformas normais se comportem de maneira previsível? No cenário descrito, eu diria que a resposta é sim.

  3. Poderiam alguns escritores de compiladores obtusos esticar a interpretação do Padrão de modo a justificar fazer algo estranho? Eu esperava que não, mas não descartaria.

  4. A higienização de compiladores deve reclamar do comportamento? Isso dependeria do nível de paranóia de seus usuários; um compilador de limpeza provavelmente não deveria reclamar por padrão sobre tal comportamento, mas talvez forneça uma opção de configuração para fazer no caso de programas serem portados para compiladores "inteligentes" / burros que se comportam de maneira estranha.

Se uma interpretação razoável do Padrão implicaria que um comportamento é definido, mas alguns redatores do compilador ampliam a interpretação para justificar o contrário, realmente importa o que o Padrão diz?

supergato
fonte
1. Não é incomum para os programadores achar que as suposições feitas por otimizadores modernos / agressivos estão em desacordo com o que eles consideram "razoável" ou "qualidade". 2. Quando se trata de ambigüidades na especificação, não é incomum que os implementadores discordem quanto às liberdades que podem assumir. 3. Quando se trata de membros do comitê de padrões C, mesmo eles nem sempre concordam quanto a qual é a interpretação "correta", muito menos qual deveria ser. Diante do exposto, que interpretação razoável devemos seguir?
Dror K.
6
Responder à pergunta "esse código específico invoca o UB ou não" com uma dissertação sobre o que você pensa sobre a utilidade do UB ou como os compiladores devem se comportar é uma tentativa pobre de responder, especialmente porque você pode copiar e colar como uma resposta a quase todas as perguntas sobre UB em particular. Como uma réplica ao seu floreio retórico: sim, realmente importa o que o padrão diz, não importa o que alguns redatores de compiladores façam ou o que você pense deles por fazer isso, porque o padrão é o que os programadores e redatores de compiladores começam.
Jeroen Mostert
1
@JeroenMostert: A resposta para "O X invoca um comportamento indefinido" frequentemente dependerá do que se quer dizer com a pergunta. Se um programa é considerado como tendo comportamento indefinido se o padrão não impõe requisitos sobre o comportamento de uma implementação conforme, quase todos os programas invocam o UB. Os autores do Padrão permitem claramente que as implementações se comportem de maneira arbitrária se um programa aninha as chamadas de função muito profundamente, desde que uma implementação possa processar corretamente pelo menos um texto-fonte (possivelmente planejado) que exerça os limites de tradução no Stadard.
supercat de
@supercat: muito interessante, mas é printf("%p", (void*) 0)comportamento indefinido ou não, de acordo com o padrão? Chamadas de função profundamente aninhadas são tão relevantes quanto o preço do chá na China. E sim, UB é muito comum em programas do mundo real - e daí?
Jeroen Mostert
1
@JeroenMostert: Uma vez que o Padrão permitiria uma implementação obtusa para considerar quase qualquer programa como tendo UB, o que deveria importar será o comportamento das implementações não obtusas. Caso você não tenha percebido, eu não escrevi apenas um copy / paste sobre o UB, mas respondi a pergunta sobre %ppara cada possível significado da pergunta.
supercat de