Projeto de fim de semana: um desafio divertido em C
(lcamtuf.substack.com)- C é simples e expressivo, mas só com a instrução switch e as regras de rótulos já é possível criar código capaz de deixar até desenvolvedores experientes perplexos
- A ofuscação ao estilo IOCCC esconde o código com macros e formatação, mas a própria sintaxe de C também permite criar código legível, porém estranho
switch (...)não exige chaves e, como salta para o rótulo case correspondente, pode se comportar de forma diferente do fluxo comum de inicialização de blocos- Rótulos
casenão precisam ficar apenas no nível superior do blocoswitch, então até um switch sem chaves que combinaif (0) case...com uma cadeia deelse ifcompila - A extensão de endereços de rótulos
&&do GNU C permite até criar switches próprios ou loops baseados em rótulos, mas alguns exemplos são específicos do GCC e podem não ser seguros contra comportamento indefinido
Código estranho criado pela sintaxe de C
- C tem muitos defeitos, mas, graças à sua sintaxe simples e expressividade, é usado como uma linguagem capaz de escrever grandes softwares, como sistemas operacionais
- A sintaxe concisa influenciou a estrutura de código de muitas linguagens mainstream posteriores, de Java a Go
- O exemplo clássico de ofuscação de código é o IOCCC
- Obras vencedoras do IOCCC frequentemente são compostas por macros do pré-processador, formatação anormal, nomes de variáveis pouco úteis e expressões aritméticas obscuras
- Esse tipo de código pode ser admirável, mas pode ser menos divertido para aprender, pois é preciso fazer engenharia reversa para transformá-lo em código normal
As profundezas de C reveladas por switch, case e goto
switch (...)pode ser usado sem chaves, assim comoif (...)oufor (...)switch (i) case 1: puts("i = 1");compila- Sem chaves, apenas uma instrução fica associada ao
switch, então ocase 2:seguinte deixa de estar dentro doswitche vira erro
switché, essencialmente, uma estrutura parecida com goto que se desloca para o rótulocasecorrespondente- Mesmo que haja
int a = 123;eputs(...)dentro do blocoswitch, ao saltar paradefault:, o código de inicialização anterior não é executado - Nesse caso,
anão é inicializado com 123 e, tecnicamente, isso se torna comportamento indefinido - O exemplo pode ser conferido no Godbolt
- Mesmo que haja
- Rótulos
casenão precisam ficar apenas no nível superior do blocoswitchassociado- Uma forma como
if (0) case 0: puts("i = 0");também funciona - Como o
switchsalta diretamente para essecase, oif (0)anterior é ignorado - As outras instruções de saída após a execução desse
puts(...)continuam bloqueadas pela condiçãoif (0), evitando fallthrough mesmo sembreak - O exemplo pode ser conferido no Godbolt
- Uma forma como
- Uma cadeia
if ... elsese comporta sintaticamente como uma única instrução de nível superior, então é possível combiná-la comswitchpara criar um switch bizarro sem chaves- O exemplo inclui até um case de intervalo
case 1 ... 10edefault - O exemplo pode ser conferido no Godbolt
- O exemplo inclui até um case de intervalo
- O operador
&&, uma extensão GNU, permite obter o endereço de um rótulo e dargotopara esse endereço- Com isso, é possível implementar um switch próprio de uma forma como
goto *(void*[]){ &&case_0, &&case_1, &&case_2 }[i]; - O exemplo pode ser conferido no Godbolt
- Com isso, é possível implementar um switch próprio de uma forma como
- Usando a mesma extensão GNU, também é possível implementar, dentro de uma declaração de variável, um loop baseado em rótulos sem
for (...)- O exemplo constrói um loop que imprime de
i = 0ai = 5usando rótulos egoto *&&_ - O exemplo pode ser conferido no Godbolt
- Este último snippet é específico do GCC e possivelmente não é seguro contra comportamento indefinido
- O exemplo constrói um loop que imprime de
- Em C, mesmo sem ofuscar código intencionalmente, é possível criar formas completamente estranhas e confusas
1 comentários
Comentários no Hacker News
O exemplo acima diz que imprime o valor de
a, mas que ele não é inicializado com 123; em C, porém, isso pode de fato acontecerUsar uma variável não inicializada não significa ler “o valor que por acaso ficou naquela memória”; é comportamento indefinido, então o compilador pode fazer o que quiser
Por exemplo, ele pode inicializar essa memória incondicionalmente com 123, ou considerar que o trecho inteiro tem comportamento indefinido e remover todas as instruções, fazendo com que nada seja impresso. Indo além, também pode otimizar e remover o
returnseguinte ou até instruções anteriores, de modo que o comportamento indefinido possa parecer afetar as coisas “voltando no tempo”Ele pode afetar instruções anteriores, mas reordenação de código e transformações complexas acontecem mesmo sem comportamento indefinido
Objetos estáticos são sempre inicializados, então essa situação não ocorre
O que sobra são objetos dinâmicos, como membros não inicializados de uma struct alocada com
malloc; ler memória dinâmica não inicializada não é comportamento indefinido em C, e você obtém o valor representado pelos bits não inicializados. Se o tipo não tiver representações de trap, isso não pode falharSintaxes desse tipo também são possíveis
switch(k) { if (0) case 0: x = 1; if (0) case 1: x = 2; if (0) default: x = 3; }Dá para fazer algo parecido com um
switchem que não é preciso escreverbreakao fim de cada cláusula, e até criar uma macro como#define brkcase if (0) case. O compilador provavelmente não vai gostar do fluxo de controle, mas em geral deve conseguir eliminar isso bem#define brkcase break;casefuncionaria de modo parecido, mas aí o propósito da macro meio que desaparecegotodo que usar esse abuso de switch no estilo Duff's devicecasetem uma única linha ou está envolvido por chavesNo passado, já usei essa estrutura com o sentido de “pular apenas a primeira linha do próximo rótulo
casee deixar o resto fazer fallthrough normalmente”Se você enxergar rótulos
casecomo simples rótulos, e não como separadores entre comandos, tudo faz sentidoDá para usar essa técnica para implementar corrotinas em C: https://stackoverflow.com/questions/24202890/switch-based-co...
Não sei como eu não sabia que
case 1 ... 10:é C válidoUsei C por anos e fico curioso para saber de qual padrão isso veio
Não encontrei a história da extensão e, até onde sei, ela não existe no C padrão. Não tenho certeza quanto ao
clangUma vez escrevi por diversão um código C estranho que fazia uma contagem regressiva de 10 até 1
As versões em C, Python e shell podem ser feitas em uma única linha usando
&&e chamadas recursivassys.stdout.write(f"{n}\n")pode ser trocado porprint(n)Do jeito que está, tirando a
f-string, o código parece no estilo do Python 2, quandoprintera uma instrução. No Python 3,printé uma função comum e retornaNone, que é avaliado como falso, então o primeiroandtambém precisa ser trocado pororOutra coisa surpreendente é que
4[arr]é igual aarr[4]*(array_label+offset), e aqui vira*(offset+array_label)Ou seja,
*(arr+4)e*(4+arr)são a mesma coisaarr[i][j]ej[i[arr]]também são exatamente iguaisBasta saber que
a[x][y]é o mesmo que(a[x])[y], e quea[x]é o mesmo quex[a]arr[i][j]→(arr[i])[j]→(i[arr])[j]→j[i[arr]]O último trecho de código ofuscado do texto mostra outra extensão do GCC: https://stackoverflow.com/questions/34559705/ternary-conditi...
Vi primeiro no Twitter esses truques brincalhões do autor deste blog
Também dá para criar loops com uma instrução
switch: https://twitter.com/lcamtuf/status/1807129116980007037Acho que essas brincadeiras com
switchsão uma parte importante do Duff's deviceswitche loops, na escolha de que umcasefaça fallthrough se não houverbreakexplícito, e no fato de que, em C, um loop pode saltar de volta para dentro de umswitchDuff estava tentando otimizar entrada/saída mapeada em memória (MMIO), e hoje em dia ninguém faria isso desse jeito em C. Agora MMIO não é tão rápida quanto a velocidade das instruções da CPU, e, se houver um pouco mais de dados, dá para usar DMA
Em uma linguagem moderna, MMIO provavelmente nem seria tratada como uma simples indireção por ponteiro, mas em C continuaram adicionando mecanismos de escape ao sistema de tipos para manter isso
Pessoalmente, vejo o mecanismo de iterate loops do WUFFS como o sucessor do “Device” de Tom Duff. Ele permite especificar uma forma de desenrolar parcialmente N etapas de um loop, prometendo o mesmo resultado que executar o corpo principal do loop N vezes, mas possivelmente mais rápido. A vetorização fica mais fácil de inferir, e casos de borda incômodos como
M % N != 0passam a ser tratados corretamente por ferramentas, não por pessoas