Redefinindo NULL

118

Estou escrevendo o código C para um sistema em que o endereço 0x0000 é válido e contém E / S de porta. Portanto, quaisquer possíveis bugs que acessem um ponteiro NULL permanecerão não detectados e ao mesmo tempo causarão um comportamento perigoso.

Por esse motivo, desejo redefinir NULL como outro endereço, por exemplo, um endereço que não é válido. Se eu acidentalmente acessar esse endereço, terei uma interrupção de hardware onde posso lidar com o erro. Acontece que tenho acesso a stddef.h para este compilador, então posso realmente alterar o cabeçalho padrão e redefinir NULL.

Minha pergunta é: isso entrará em conflito com o padrão C? Pelo que posso dizer a partir do 7.17 no padrão, a macro é definida pela implementação. Há algo em outro lugar no padrão afirmando que NULL deve ser 0?

Outro problema é que muitos compiladores executam inicialização estática definindo tudo como zero, não importa o tipo de dados. Mesmo que o padrão diga que o compilador deve definir inteiros para zero e ponteiros para NULL. Se eu redefinisse NULL para meu compilador, então sei que essa inicialização estática falhará. Eu poderia considerar isso um comportamento incorreto do compilador, embora eu ousadamente alterasse os cabeçalhos do compilador manualmente? Porque eu sei com certeza que este compilador específico não acessa a macro NULL ao fazer a inicialização estática.

Lundin
fonte
3
Esta é uma pergunta muito boa. Não tenho uma resposta para você, mas tenho que perguntar: você tem certeza de que não é possível mover suas coisas válidas em 0x00 para longe e deixar NULL ser um endereço inválido como em sistemas "normais"? Se você não puder, os únicos endereços inválidos com segurança a serem usados ​​serão aqueles que você pode ter certeza de que pode alocar e depois mprotectproteger. Ou, se a plataforma não tiver ASLR ou similar, endereços além da memória física da plataforma. Boa sorte.
Borealid
8
Como funcionará se o seu código estiver usando if(ptr) { /* do something on ptr*/ }? Funcionará se NULL for definido diferente de 0x0?
Xavier T.
3
O ponteiro C não tem relação forçada com endereços de memória. Desde que as regras da aritmética de ponteiros sejam obedecidas, um valor de ponteiros pode ser qualquer coisa. A maioria das implementações escolhe usar os endereços de memória como valores de ponteiro, mas eles podem usar qualquer coisa, desde que seja um isomorfismo.
datenwolf
2
@bdonlan Isso violaria as regras (consultivas) no MISRA-C também.
Lundin
2
@Andreas Sim, esse é o meu pensamento também. Não se deve permitir que o pessoal de hardware projete hardware em que o software deva ser executado! :)
Lundin

Respostas:

84

O padrão C não requer que ponteiros nulos estejam no endereço zero da máquina. NO ENTANTO, lançar uma 0constante para um valor de ponteiro deve resultar em um NULLponteiro (§6.3.2.3 / 3), e avaliar o ponteiro nulo como um booleano deve ser falso. Isso pode ser um pouco estranho se você realmente não quer um endereço zero, e NULLnão é o endereço zero.

No entanto, com modificações (pesadas) no compilador e na biblioteca padrão, não é impossível NULLser representado com um padrão de bits alternativo enquanto permanece estritamente em conformidade com a biblioteca padrão. É não suficiente para simplesmente mudar a definição de NULLsi no entanto, como então NULLseria avaliada como verdadeira.

Especificamente, você precisa:

  • Faça com que zeros literais em atribuições a ponteiros (ou conversões em ponteiros) sejam convertidos em algum outro valor mágico, como -1.
  • Faça testes de igualdade entre ponteiros e um número inteiro constante 0para verificar o valor mágico (§6.5.9 / 6)
  • Organize todos os contextos nos quais um tipo de ponteiro é avaliado como booleano para verificar a igualdade do valor mágico em vez de verificar o zero. Isso decorre da semântica do teste de igualdade, mas o compilador pode implementá-lo de forma diferente internamente. Consulte §6.5.13 / 3, §6.5.14 / 3, §6.5.15 / 4, §6.5.3.3 / 5, §6.8.4.1 / 2, §6.8.5 / 4
  • Como caf apontou, atualize a semântica para inicialização de objetos estáticos (§6.7.8 / 10) e inicializadores compostos parciais (§6.7.8 / 21) para refletir a nova representação de ponteiro nulo.
  • Crie uma maneira alternativa de acessar o endereço zero verdadeiro.

Existem algumas coisas com as quais você não precisa lidar. Por exemplo:

int x = 0;
void *p = (void*)x;

Depois disso, pNÃO é garantido que seja um ponteiro nulo. Apenas atribuições constantes precisam ser tratadas (esta é uma boa abordagem para acessar o endereço zero verdadeiro). Da mesma forma:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

Além disso:

void *p = NULL;
int x = (int)p;

xnão é garantido que seja 0.

Em suma, essa mesma condição foi aparentemente considerada pelo comitê de linguagem C e considerações feitas para aqueles que escolheriam uma representação alternativa para NULL. Tudo o que você precisa fazer agora é fazer grandes alterações em seu compilador e pronto, pronto :)

Como uma observação lateral, pode ser possível implementar essas mudanças com um estágio de transformação do código-fonte antes do compilador apropriado. Ou seja, em vez do fluxo normal do pré-processador -> compilador -> montador -> vinculador, você adicionaria um pré-processador -> transformação NULL -> compilador -> montador -> vinculador. Então você pode fazer transformações como:

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

Isso exigiria um analisador C completo, bem como um analisador de tipo e análise de typedefs e declarações de variáveis ​​para determinar quais identificadores correspondem a ponteiros. No entanto, ao fazer isso, você pode evitar ter que fazer alterações nas partes de geração de código do compilador adequado. O clang pode ser útil para implementar isso - entendo que foi projetado com transformações como essa em mente. Você provavelmente ainda precisará fazer alterações na biblioteca padrão também, é claro.

bdonlan
fonte
2
Ok, não encontrei o texto em §6.3.2.3, mas suspeitei que haveria tal declaração em algum lugar :). Acho que isso responde à minha pergunta, pelo padrão não tenho permissão para redefinir NULL, a menos que queira escrever um novo compilador C para me apoiar :)
Lundin
2
Um bom truque é hackear o compilador para que o ponteiro <-> conversões inteiras XOR um valor específico que é um ponteiro inválido e ainda trivial o suficiente para que a arquitetura alvo possa fazer isso de forma barata (normalmente, seria um valor com um único bit definido , como 0x20000000).
Simon Richter
2
Outra coisa que você precisa mudar no compilador é a inicialização de objetos com tipo composto - se um objeto for parcialmente inicializado, então quaisquer ponteiros para os quais um inicializador explícito não esteja presente devem ser inicializados NULL.
caf
20

O padrão afirma que uma expressão de constante inteira com valor 0, ou uma expressão convertida para o void *tipo, é uma constante de ponteiro nula. Isso significa que (void *)0é sempre um ponteiro nulo, mas int i = 0;, dado , (void *)inão precisa ser.

A implementação C consiste no compilador junto com seus cabeçalhos. Se você modificar os cabeçalhos para redefinir NULL, mas não modificar o compilador para corrigir inicializações estáticas, terá criado uma implementação não conforme. É toda a implementação em conjunto que apresenta comportamento incorreto e, se você a quebrou, não tem mais ninguém para culpar;)

Você deve corrigir mais do que apenas inicializações estáticas, é claro - dado um ponteiro p, if (p)é equivalente a if (p != NULL), devido à regra acima.

caf
fonte
8

Se você usar a biblioteca C std, terá problemas com funções que podem retornar NULL. Por exemplo, a documentação do malloc afirma:

Se a função falhar ao alocar o bloco de memória solicitado, um ponteiro nulo é retornado.

Como malloc e as funções relacionadas já estão compiladas em binários com um valor NULL específico, se você redefinir NULL, não será capaz de usar diretamente a biblioteca C std, a menos que possa reconstruir sua cadeia de ferramentas inteira, incluindo as bibliotecas C std.

Além disso, devido ao uso de NULL pela biblioteca std, se você redefinir NULL antes de incluir cabeçalhos std, poderá sobrescrever uma definição NULL listada nos cabeçalhos. Qualquer coisa embutida seria inconsistente dos objetos compilados.

Em vez disso, eu definiria seu próprio NULL, "MYPRODUCT_NULL", para seus próprios usos e evitaria ou traduziria de / para a biblioteca C std.

Doug T.
fonte
6

Deixe NULL sozinho e trate o IO para a porta 0x0000 como um caso especial, talvez usando uma rotina escrita em assembler e, portanto, não sujeito à semântica C padrão. IOW, não redefina NULL, redefina a porta 0x00000.

Observe que se você está escrevendo ou modificando um compilador C, o trabalho necessário para evitar a desreferenciação de NULL (assumindo que no seu caso a CPU não está ajudando) é o mesmo, não importa como NULL seja definido, então é mais fácil deixar NULL definido como zero e certifique-se de que o zero nunca pode ser desreferenciado de C.

Apalala
fonte
O problema só surgirá quando NULL for acessado acidentalmente, não quando a porta for acessada intencionalmente. Por que eu redefiniria a porta I / O para então? Já está funcionando como deveria.
Lundin
2
@Lundin Acidentalmente ou não, NULL pode ser desreferenciado em um programa C usando *p, p[]ou p(), portanto, o compilador só precisa se preocupar com aqueles para proteger a porta IO 0x0000.
Apalala
@Lundin A segunda parte da sua pergunta: Depois de restringir o acesso ao endereço zero de dentro de C, você precisa de outra maneira de alcançar a porta 0x0000. Uma função escrita em assembler pode fazer isso. De dentro de C, a porta pode ser mapeada para 0xFFFF ou qualquer outro, mas é melhor usar uma função e esquecer o número da porta.
Apalala
3

Considerando a extrema dificuldade em redefinir NULL conforme mencionado por outros, talvez seja mais fácil redefinir a desreferenciação para endereços de hardware bem conhecidos. Ao criar um endereço, adicione 1 a cada endereço conhecido, de modo que sua porta IO conhecida seja:

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

Se os endereços com os quais você está preocupado estiverem agrupados e você puder se sentir seguro de que adicionar 1 ao endereço não entrará em conflito com nada (o que não deveria ocorrer na maioria dos casos), você poderá fazer isso com segurança. E então você não precisa se preocupar em reconstruir sua cadeia de ferramentas / std lib e expressões no formato:

  if (pointer)
  {
     ...
  }

ainda funciona

Louco eu sei, mas pensei em lançar a ideia por aí :).

Doug T.
fonte
O problema só surgirá quando NULL for acessado acidentalmente, não quando a porta for acessada intencionalmente. Por que eu redefiniria a porta I / O para então? Já está funcionando como deveria.
Lundin
@LundIn Eu acho que você tem que escolher o que é mais doloroso, refazer a reconstrução de todo o conjunto de ferramentas ou alterar essa parte do seu código.
Doug T.
2

O padrão de bits para o ponteiro nulo pode não ser o mesmo que o padrão de bits para o inteiro 0. Mas a expansão da macro NULL deve ser uma constante de ponteiro nulo, que é um inteiro constante de valor 0 que pode ser convertido em (void *).

Para alcançar o resultado desejado sem perder a conformidade, você terá que modificar (ou talvez configurar) sua cadeia de ferramentas, mas é possível.

AProgrammer
fonte
1

Você está procurando problemas. Redefinir NULLpara um valor não nulo quebrará este código:

   if (myPointer)
   {
      // myPointer não é nulo
      ...
   }
Tony o Pônei
fonte