Algumas das características da linguagem C começaram como hacks que simplesmente funcionaram.
Múltiplas assinaturas para listas de argumentos principais, bem como de comprimento variável, é um desses recursos.
Os programadores notaram que podem passar argumentos extras para uma função e nada de ruim acontece com o compilador fornecido.
Este é o caso se as convenções de chamada forem tais que:
- A função de chamada limpa os argumentos.
- Os argumentos mais à esquerda estão mais próximos do topo da pilha ou da base da estrutura da pilha, de forma que argumentos espúrios não invalidem o endereçamento.
Um conjunto de convenções de chamada que obedece a essas regras é a passagem de parâmetro baseada em pilha, por meio da qual o chamador exibe os argumentos e eles são empurrados da direita para a esquerda:
;; pseudo-assembly-language
;; main(argc, argv, envp); call
push envp ;; rightmost argument
push argv ;;
push argc ;; leftmost argument ends up on top of stack
call main
pop ;; caller cleans up
pop
pop
Em compiladores onde esse tipo de convenção de chamada é o caso, nada de especial precisa ser feito para oferecer suporte aos dois tipos main
, ou mesmo tipos adicionais. main
pode ser uma função sem argumentos e, nesse caso, ignora os itens que foram colocados na pilha. Se for uma função de dois argumentos, ele encontra argc
e argv
como os dois itens mais altos da pilha. Se for uma variante de três argumentos específica da plataforma com um ponteiro de ambiente (uma extensão comum), isso também funcionará: encontrará o terceiro argumento como o terceiro elemento do topo da pilha.
E assim, uma chamada fixa funciona para todos os casos, permitindo que um único módulo fixo de inicialização seja vinculado ao programa. Esse módulo pode ser escrito em C, como uma função semelhante a esta:
/* I'm adding envp to show that even a popular platform-specific variant
can be handled. */
extern int main(int argc, char **argv, char **envp);
void __start(void)
{
/* This is the real startup function for the executable.
It performs a bunch of library initialization. */
/* ... */
/* And then: */
exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}
Em outras palavras, este módulo inicial apenas chama um principal de três argumentos, sempre. Se main não leva argumentos, ou apenas int, char **
, funciona bem, assim como se não leva argumentos, devido às convenções de chamada.
Se você fizesse esse tipo de coisa em seu programa, seria não portável e considerado um comportamento indefinido pelo ISO C: declarar e chamar uma função de uma maneira e defini-la de outra. Mas o truque de inicialização de um compilador não precisa ser portátil; não é guiado pelas regras para programas portáteis.
Mas suponha que as convenções de chamada sejam tais que não funcione dessa maneira. Nesse caso, o compilador deve tratar de forma main
especial. Quando percebe que está compilando a main
função, pode gerar código compatível com, digamos, uma chamada de três argumentos.
Ou seja, você escreve isto:
int main(void)
{
/* ... */
}
Mas quando o compilador o vê, ele essencialmente executa uma transformação de código para que a função que ele compila se pareça mais com isto:
int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
/* ... */
}
exceto que os nomes __argc_ignore
não existem literalmente. Esses nomes não são introduzidos em seu escopo e não haverá nenhum aviso sobre argumentos não utilizados. A transformação do código faz com que o compilador emita o código com a ligação correta, que sabe que deve limpar três argumentos.
Outra estratégia de implementação é o compilador ou talvez o linker gerar a __start
função de forma personalizada (ou o que quer que seja chamado), ou pelo menos selecionar uma das várias alternativas pré-compiladas. As informações podem ser armazenadas no arquivo de objeto sobre qual dos formulários suportados main
está sendo usado. O vinculador pode consultar essas informações e selecionar a versão correta do módulo de inicialização que contém uma chamada main
compatível com a definição do programa. Implementações de C geralmente têm apenas um pequeno número de formas com suporte de, main
portanto, essa abordagem é viável.
Compiladores para a linguagem C99 sempre têm que tratar main
especialmente, até certo ponto, para suportar o hack de que se a função terminar sem uma return
instrução, o comportamento é como se tivesse return 0
sido executado. Isso, novamente, pode ser tratado por uma transformação de código. O compilador percebe que uma função chamada main
está sendo compilada. Em seguida, ele verifica se a extremidade do corpo é potencialmente alcançável. Nesse caso, ele insere umreturn 0;
main
método em um único programa emC
(ou, na verdade, em praticamente qualquer linguagem com tal construção).