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:
int main(void)
{
  // ...
}int main(int argc, char *argv[])
{
  // ...
}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
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:
$ objdump -d /usr/lib/x86_64-linux-gnu/Scrt1.oFazendo seu próprio startup code
Use o seguinte código de teste:
STDOUT_FILENO = 1
SYS_WRITE = 1
SYS_EXIT_GROUP = 231
    .section .rodata
init_msg:
    .string "* Initializing...\n"
    MSG_LENGHT = . - init_msg
    .text
    .globl _start
_start:
    mov $STDOUT_FILENO, %rdi
    lea init_msg(%rip), %rsi
    mov $MSG_LENGHT, %rdx
    mov $SYS_WRITE, %rax
    syscall         # write(STDOUT_FILENO, init_msg, MSG_LENGTH)
    pop %rdi        # argc: RDI
    mov %rsp, %rsi  # argv: RSI
    call main
    mov %rax, %rdi
    mov $SYS_EXIT_GROUP, %rax
    syscall         # exit_group( main(argc, argv) )#include <stdio.h>
int main(int argc, char **argv)
{
  printf("argc = %d\n", argc);
  for (int i = 0; i < argc; i++)
  {
    printf("argv[%d] = '%s'\n", i, argv[i]);
  }
  // Esperamos que argv[argc] seja um ponteiro NULL
  printf("argv[argc] = %s\n", argv[argc]);
  return 0;
}Compile com:
$ as start.s -o crt1.o
$ gcc main.c -o main.o -c
$ gcc *.o -o test -nostartfilesA 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.
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:
#include <stdio.h>
__attribute__((constructor))
void constructor1(void)
{
  puts("* constructor 1");
}
__attribute__((constructor))
void constructor2(void)
{
  puts("* constructor 2");
}
__attribute__((destructor))
void destructor1(void)
{
  puts("* destructor 1");
}
__attribute__((destructor))
void destructor2(void)
{
  puts("* destructor 2");
}
int main(void)
{
  puts("* main");
}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:
	.section	.init_array,"aw"
	.align 8
	.quad	constructor1Funçõ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:
void funcname(void);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:
#include <stdio.h>
#include <stdlib.h>
void func_atexit(void)
{
  puts("* exiting...");
}
void func_at_quick_exit(void)
{
  puts("* Quick exiting...");
}
int main(void)
{
  atexit(func_atexit);
  at_quick_exit(func_at_quick_exit);
  puts("* main");
  // quick_exit(EXIT_SUCCESS);
  return 0;
}Experimente executar o programa acima e depois recompilar com a chamada para quick_exit na linha 20.
_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
Was this helpful?
