Ambiente hosted
Entendendo a execução de código em C no ambiente hosted.
Na especificação da linguagem C é descrito dois ambientes de execução de código: Os ambientes hosted e freestanding. Neste tópico vamos entender alguns pontos em relação a como funciona a estrutura e a execução de um programa em C no ambiente hosted.
O ambiente hosted essencialmente é o ambiente de execução de um código em C que executa sobre um sistema operacional. Nesse ambiente é esperado que haja suporte para múltiplas threads e todos os recursos descritos na especificação da biblioteca padrão (libc). A inicialização do programa ocorre quando a função main é chamada e antes de inicializar o programa é esperado que todos os objetos com storage-class static
estejam inicializados.
A função main
A função main pode ser escrita com um dos dois protótipos abaixo:
Ou qualquer outro protótipo que seja equivalente a um desses. Como por exemplo char **argv
também seria válido por ter equivalência a char *argv[]
. Também pode-se usar qualquer nome de parâmetro, argc
e argv
são apenas sugestões.
O primeiro parâmetro passado para a função main indica o número de argumentos e o segundo é uma array de ponteiros para char
onde cada índice na array é um argumento e argv[argc]
é um ponteiro NULL.
Se o tipo de retorno da função main for int
(ou equivalente), o valor de retorno da primeira chamada para main é equivalente a chamar a função exit passando esse valor como argumento.
C startup code
Os detalhes de implementação descritos aqui são baseados no código-fonte da glibc e podem ser diferentes em outras implementações da libc. Consulte as referências para ver a lista de completa de arquivos fonte consultados.
O código na glibc responsável pela inicialização do programa é chamado de C startup (CSU). Ele se encarrega de obter os argumentos de linha de comando, inicializar o TLS, executar o código na seção .init
dentre outras tarefas de inicialização do programa.
O arquivo start.S
é o que declara o símbolo _start
, ou seja, a função de entry point do programa. A última chamada nessa função é para outra função chamada __libc_start_main
que recebe o endereço da função main como primeiro argumento. Depois de algumas inicializações essa função chama a main, obtém o valor retornado em EAX e passa como argumento para a função responsável por finalizar o programa no sistema operacional (exit_group
no Linux e ExitProcess
no Windows).
Todos esses códigos estão em arquivos objetos pré-compilados no seu sistema operacional. Eles são linkados por padrão quando você invoca o GCC mas não são linkados por padrão se você chamar o linker (ld
) diretamente.
No meu Linux o arquivo objeto Scrt1.o
("crt" é sigla para "C runtime") é o que contém o entry point (código do start.S
). Os arquivos crti.o
e crtn.o
contém o prólogo e o epílogo, respectivamente, para as seções .init
e .fini
.
No meu Linux esses arquivos estão na pasta /usr/lib/x86_64-linux-gnu/
e sugiro que consulte o conteúdo dos mesmos com a ferramenta objdump, como por exemplo:
Fazendo seu próprio startup code
Apenas para fins de curiosidade e dar uma noção mais "palpável" de como isso ocorre, irei ensinar aqui como você pode desabilitar a linkedição do CSU e programar uma versão personalizada do mesmo no Linux. Não recomendo que isso seja feito em um programa de verdade tendo em vista que você perderá diversos recursos que o C runtime padrão da glibc provém.
Use o seguinte código de teste:
Compile com:
A opção -nostartfiles
desabilita a linkedição dos arquivos objeto de inicialização.
O que o nosso start.s
está fazendo é simplesmente chamar a syscall write
para escrever uma mensagem na tela, chama a função main passando argc
e argv
como argumentos e depois chama a syscall exit_group
passando como argumento o retorno da função main.
No Linux, logo quando o programa é iniciado no entry point, o valor contendo o número de argumentos de linha de comando (argc) está em (%rsp)
. E logo em seguida (RSP+8) está o início da array de ponteiros para os argumentos de linha de comando, terminando com um ponteiro NULL.
Experimente rodar objdump -d test
nesse executável "customizado" e depois compare compilando com o CSU comum. Verá que o programa comum contém diversas funções que foram linkadas nele.
Seções .init e .fini
As seções .init
e .fini
contém funções construída nos arquivos crti.o
e crtn.o
.
O propósito da função em .init
é chamar todas as funções na array de ponteiros localizada em outra seção chamada .init_array
. Essas funções são invocadas antes da chamada para a função main.
Já a função em .fini
invoca as funções da array na seção .fini_array
na finalização do programa (após main retornar ou na chamada de exit()
).
No GCC você pode adicionar funções para serem invocadas na inicialização do programa com o atributo constructor
, e para a finalização do programa com o atributo destructor
. Experimente ver o código Assembly do exemplo abaixo:
Ao ver o Assembly gerado do programa acima irá notar que os endereços das funções são despejados nas seções .init_array
e .fini_array
, como em:
Funções de saída
exit
Quando a função exit()
é invocada (ou main retorna), funções registradas pela função atexit()
são executadas. Onde as funções registradas devem seguir o protótipo:
As funções registradas por atexit()
são invocadas na ordem inversa a que foram registradas.
quick_exit
Quando a função quick_exit()
é invocada o programa é finalizado sem invocar as funções registradas por atexit()
e sem executar quaisquer handlers de sinal.
As funções registradas por at_quick_exit
são invocadas na ordem inversa em que foram registradas.
Exemplo:
Experimente executar o programa acima e depois recompilar com a chamada para quick_exit na linha 20.
A quantidade máxima de funções que podem ser registradas com atexit ou at_quick_exit depende da implementação. Mas a especificação do C11 garante que no mínimo 32 funções podem ser registradas por cada uma destas funções.
_Exit
A função _Exit()
finaliza a execução do programa sem executar quaisquer funções registradas por atexit ou at_quick_exit. Também não executa nenhum handler de sinal.
Last updated