Quando um programa é executado, ele alterna várias vezes entre o modo de usuário e o modo kernel . No modo de usuário, um processo tem acesso limitado aos recursos, enquanto no modo kernel ele tem acesso aos recursos de hardware privilegiados e seus dados. Um processo alterna do modo de usuário para o modo kernel usando chamadas do sistema .

Strace é uma ferramenta para analisar as atividades de chamada do sistema de um processo. Fornece informações sobre:

  • Arquivos acessados.
  • Chamadas de sistema usadas durante a execução
  • Tempo gasto por cada chamada de sistema para um processo etc.

Analisar chamadas de sistema ajuda muito quando você não tem acesso ao código-fonte e a depuração é feita usando apenas binários executáveis. Este artigo não é sobre como usar a ferramenta Strace , é mais sobre como analisar a saída da ferramenta Strace porque, ao executar o Strace em um processo, ela despeja muitas informações relacionadas às chamadas do sistema. À primeira vista, parece muito assustador e analisar cada chamada de sistema seria uma tarefa muito demorada. Além disso, pode não ser necessário porque a maioria das chamadas de sistema iniciais são para fins de manutenção e não agregam muito valor à depuração. Uma vez que o fluxo de chamadas do sistema é compreendido para um processo, então ele pode ser facilmente identificado e remover chamadas do sistema de manutenção e se concentrar em uma importante para depurar nosso problema real.

Programa:

// C program to print Hello World!
// filename: hello.c
#include <stdio.h>
  
// Driver Code
int main(int argc, char* argv[])
{
    // Print Hello World
    printf(" geeksforgeeks: hello world !! \n");
  
    return 0;
}
Saída:

geeksforgeeks: olá, mundo !!

Compile o programa acima usando o comando abaixo:

$ gcc hello.c

Encontre o Strace fora do programa compilado acima usando o comando abaixo:

$ strace ./a.out

Agora, a saída do Strace para o programa acima é:

Antes de começar a analisar as chamadas do sistema, vamos falar brevemente sobre a execução do programa em chamadas do sistema:

  • O programa “Hello World” será aberto e mapas de memória para todas as bibliotecas compartilhadas na memória virtual do processo. A maioria das chamadas do sistema está relacionada a esta atividade.
  • Define o acesso correto para as seções de memória.
  • E finalmente executa o programa, que irá escrever a mensagem “geeksforgeeks: hello world !!” em stdout do processo.

Saída de decodificação do Strace :

Agora divida a saída do Strace em partes significativas para melhor compreensão:

  • execve():

    Quando executamos o executável ./a.out no console bash, um novo processo filho é bifurcado e execve() é executado para carregar o novo programa “a.out” . Possui 3 parâmetros:

    • O primeiro parâmetro é o nome do executável
    • O segundo parâmetro é uma array de argumentos do executável, dos quais o primeiro argumento é o próprio nome do executável. Como nenhum argumento é fornecido ao executável, vemos apenas ./a.out na lista de argumentos.
    • O terceiro parâmetro é uma string de variáveis ​​de ambiente.

    Aqui, o valor de retorno é 0 , o que significa sucesso. Mais detalhes sobre a chamada de sistema execve() podem ser encontrados na página de manual de execve usando “man 2 execve” .

  • brk(): Esta chamada de sistema define o tamanho do segmento de dados para o endereço especificado. Aqui, brk (NULL) é usado para obter o topo do endereço do segmento de dados, que é o endereço inicial do heap. Conseqüentemente, chamar brk() com NULL retorna, endereço de início de heap que mais tarde é usado para alocar memória heap.

  • access(): Esta chamada de sistema verifica as permissões de arquivo. Possui 2 parâmetros:
    • O primeiro parâmetro é um nome de arquivo para o qual a permissão deve ser verificada.
    • O segundo parâmetro é um modo, que especifica a verificação de acessibilidade. Acessibilidade de leitura, gravação e executável são verificadas para um arquivo. Aqui F_OK é para verificação de existência e R_OK é para verificação de leitura.
    • Se o valor de retorno for -1 , significa que o arquivo verificado não está presente.

  • ld.so.nohwcap: A presença deste arquivo desativa o carregamento de bibliotecas otimizadas. Nas últimas distribuições, este arquivo não está presente. Arquivo ld.so.preload contendo uma lista de arquivos de objetos compartilhados a serem carregados antes do programa.

    • openat() abre um arquivo /etc/ld.so.cache e retorna o descritor de arquivos 3 . /etc/ld.so.cache , contém a lista de diretórios nos quais as bibliotecas compartilhadas devem ser pesquisadas.
    • fstat() obtém os atributos do arquivo como modos, tamanho, timestamps de criação / modificação, etc. para o mesmo descritor de arquivo. O segundo parâmetro são os detalhes dos atributos lidos.
    • mmap() usa o tamanho de arquivo 127481, lido por fstat() e mapeia o arquivo inteiro na memória virtual do processo e retorna o endereço de memória virtual mapeado 0x7ff58cf81000.
    • Após o mapeamento bem-sucedido, o arquivo é fechado usando a chamada de sistema close() .
  • /etc/ld.so.cache: Contém a lista de diretórios nos quais as bibliotecas compartilhadas devem ser pesquisadas.

    É o mesmo que a linha 3 .

    O bloco de chamadas de sistema acima é sobre como abrir a biblioteca libc e mapeá-la na memória virtual do processo.

    • openat() abre /lib/x86_64-linux-gnu/libc.so.6 e retorna o descritor de arquivos 3 . O descritor de arquivo 3 é usado posteriormente para trabalhar no arquivo libc .
    • read() lê 832 bytes do arquivo libc.so. O segundo parâmetro é lido 832 bytes de dados, que são informações de cabeçalho do arquivo ELF e provavelmente usado para verificação do arquivo ELF.
    • fstat() obtém os atributos do arquivo libc .
    • mmap() mapeia o arquivo na memória virtual.
    • mprotect() atualiza a proteção da região da memória.
    • close() libera o descritor de arquivo, pois o arquivo é mapeado com sucesso na memória virtual do processo e não é mais necessário acessar por meio de um descritor de arquivo.
  • arch_prctl(): define o estado da thread específica da arquitetura. Aqui está configurando a base de 64 bits para o registro FS para o endereço 0x7ff58cf804c0 .



  • mprotect(): Chama para definir a proteção para diferentes regiões de memória. PROT_READ é usado para tornar as regiões de memória legíveis.

  • munmap(): Esta chamada para desmapear o arquivo /etc/ld.so.cache . O endereço 0x7ff58cf81000 foi mapeado para ld.so.cache na linha 7.

  • fstat(): Isso é feito no descritor de arquivo 1 , para obter seus atributos, já que printf() gravará dados usando o descritor stdout. Quando um processo é iniciado, ele abre 3 arquivos padrão:

    • Descritor de arquivo 0 para stdin .
    • Descritor de arquivo 1 para stdout .
    • Descritor de arquivo 2 para stderr .
  • brk(): chama para obter e definir o limite da seção de dados.
  • Isso corresponde à instrução printf() , que coloca os dados no stdout do processo usando a chamada do sistema write() .
  • O programa sai com 0 (SUCESSO) .

    Agora ele viu que a maioria das chamadas de sistema eram para preparar o processo para execução. Principalmente o mapeamento da biblioteca compartilhada estava causando a maioria das chamadas do sistema. Ele pode verificar isso executando o Strace em um binário executável estaticamente construído.

Abaixo está a saída do Strace para o programa “Hello World” construído usando a opção “-static” :

Ele pode ver que o executável estaticamente construído não chama open() , mmap() , close() etc, o que foi feito para mapear bibliotecas compartilhadas. Agora temos conhecimento suficiente sobre chamadas de sistema para analisar a saída da ferramenta Strace e para filtrar chamadas de sistema interessadas para depuração. Para qualquer detalhe de chamada do sistema, o melhor lugar seria sua página de manual. Que pode ser acessado usando o comando abaixo.

$ man 2 <System Call>