O syscall é o mecanismo pelo qual os programas do usuário interagem com o kernel do Linux, e o strace é uma ferramenta poderosa para controlá-los. Para entender melhor como o sistema operacional funciona, é útil entender como eles funcionam.
O sistema operacional pode ser dividido em dois modos de operação:
- O modo kernel é o modo privilegiado usado pelo kernel do sistema operacional.
- O modo de usuário é o modo no qual a maioria dos aplicativos de usuário é executada.
Os usuários geralmente usam utilitários de linha de comando e uma interface gráfica (GUI) para seu trabalho diário. Ao mesmo tempo, as chamadas de sistema funcionam de forma invisível em segundo plano, referindo-se ao kernel para fazer o trabalho.
As chamadas do sistema são muito semelhantes às chamadas de função no sentido de que os argumentos são passados e retornam valores. A única diferença é que as chamadas do sistema funcionam no nível do kernel, mas as funções não. A mudança do modo de usuário para o modo kernel é feita usando um mecanismo de interrupção especial .
A maioria desses detalhes são ocultados do usuário nas bibliotecas do sistema (glibc em sistemas Linux). As chamadas do sistema são genéricas por natureza, mas, apesar disso, a mecânica de sua execução depende em grande parte do hardware.
Este artigo explora vários exemplos práticos de análise de chamadas do sistema usando
strace
. Os exemplos usam Red Hat Enterprise Linux, mas todos os comandos devem funcionar em outras distribuições Linux:
[root@sandbox ~]# cat /etc/redhat-release
Red Hat Enterprise Linux Server release 7.7 (Maipo)
[root@sandbox ~]#
[root@sandbox ~]# uname -r
3.10.0-1062.el7.x86_64
[root@sandbox ~]#
Primeiro, certifique-se de ter as ferramentas necessárias instaladas em seu sistema. Você
strace
pode verificar se ele está instalado usando o comando abaixo. Para visualizar a versão, strace
execute-o com o parâmetro -V:
[root@sandbox ~]# rpm -qa | grep -i strace
strace-4.12-9.el7.x86_64
[root@sandbox ~]#
[root@sandbox ~]# strace -V
strace -- version 4.12
[root@sandbox ~]#
Se
strace
não estiver instalado, instale executando:
yum install strace
Por exemplo, crie um diretório de teste em
/tmp
e dois arquivos usando o comando touch
:
[root@sandbox ~]# cd /tmp/
[root@sandbox tmp]#
[root@sandbox tmp]# mkdir testdir
[root@sandbox tmp]#
[root@sandbox tmp]# touch testdir/file1
[root@sandbox tmp]# touch testdir/file2
[root@sandbox tmp]#
(Eu
/tmp
só uso um diretório porque todos têm acesso a ele, mas você pode usar qualquer outro diretório .)
Use o comando para
ls
verificar se os testdir
arquivos foram criados no diretório :
[root@sandbox tmp]# ls testdir/
file1 file2
[root@sandbox tmp]#
Você provavelmente usa o comando
ls
todos os dias sem perceber que as chamadas do sistema estão sendo executadas nos bastidores. É aqui que a abstração entra em jogo. É assim que funciona este comando:
-> (glibc) ->
O comando
ls
chama funções das bibliotecas do sistema Linux (glibc). Essas bibliotecas, por sua vez, chamam chamadas de sistema, que fazem a maior parte do trabalho.
Se você quiser saber quais funções foram chamadas da biblioteca glibc, use o comando
ltrace
seguido pelo comando ls testdir/
:
ltrace ls testdir/
Se
ltrace
não estiver instalado, instale:
yum install ltrace
Haverá muitas informações na tela, mas não se preocupe - falaremos disso mais tarde. Aqui estão algumas das funções de biblioteca importantes da saída
ltrace
:
opendir("testdir/") = { 3 }
readdir({ 3 }) = { 101879119, "." }
readdir({ 3 }) = { 134, ".." }
readdir({ 3 }) = { 101879120, "file1" }
strlen("file1") = 5
memcpy(0x1665be0, "file1\0", 6) = 0x1665be0
readdir({ 3 }) = { 101879122, "file2" }
strlen("file2") = 5
memcpy(0x166dcb0, "file2\0", 6) = 0x166dcb0
readdir({ 3 }) = nil
closedir({ 3 })
Examinando essa saída, você provavelmente pode entender o que está acontecendo. O diretório nomeado é
testdir
aberto usando uma função de biblioteca opendir
, seguida por chamadas para funções readdir
que leem o conteúdo do diretório. Finalmente, é chamada uma função closedir
que fecha o diretório aberto anteriormente. Por enquanto, ignore outras funções, como strlen
e memcpy
.
Como você pode ver, é fácil ver as funções da biblioteca chamadas, mas neste artigo vamos nos concentrar nas chamadas do sistema que são chamadas pelas funções da biblioteca do sistema.
Para visualizar as chamadas do sistema, use
strace
o comando ls testdir
conforme mostrado abaixo. E novamente você obtém um monte de informações incoerentes:
[root@sandbox tmp]# strace ls testdir/
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
brk(NULL) = 0x1f12000
<<< truncated strace output >>>
write(1, "file1 file2\n", 13file1 file2
) = 13
close(1) = 0
munmap(0x7fd002c8d000, 4096) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
[root@sandbox tmp]#
Como resultado da execução,
strace
você receberá uma lista de chamadas de sistema executadas durante a execução do comando ls
. Todas as chamadas do sistema podem ser divididas nas seguintes categorias:
- Gerenciamento de processos
- Gerenciamento de arquivos
- Gerenciamento de diretório e sistema de arquivos
- De outros
Há uma maneira conveniente de analisar as informações recebidas - grave a saída em um arquivo usando a opção
-o
.
[root@sandbox tmp]# strace -o trace.log ls testdir/
file1 file2
[root@sandbox tmp]#
Desta vez, não haverá dados na tela - o comando
ls
funcionará conforme o esperado, exibindo uma lista de arquivos e gravando todas as saídas strace
em um arquivo trace.log
. Para um comando simples, o ls
arquivo contém quase 100 linhas:
[root@sandbox tmp]# ls -l trace.log
-rw-r--r--. 1 root root 7809 Oct 12 13:52 trace.log
[root@sandbox tmp]#
[root@sandbox tmp]# wc -l trace.log
114 trace.log
[root@sandbox tmp]#
Dê uma olhada na primeira linha do arquivo
trace.log
:
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
- No início da linha está o nome da chamada do sistema sendo executada - execve.
- O texto entre parênteses são os argumentos passados para a chamada do sistema.
- O número após o sinal = (neste caso, 0) é o valor retornado pela chamada do sistema.
Agora, o resultado não parece muito assustador, não é? E você pode aplicar a mesma lógica para outras linhas também.
Preste atenção ao único comando que você chamou -
ls testdir
. Você sabe o nome do diretório usado pelo comando ls
, então por que não usar grep
para testdir
no arquivo trace.log
e ver o que ele encontra? Observe o resultado com atenção:
[root@sandbox tmp]# grep testdir trace.log
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
stat("testdir/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 0
openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
[root@sandbox tmp]#
Voltando à análise acima
execve
, você pode dizer o que a próxima chamada do sistema está fazendo?
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
Você não precisa se lembrar de todas as chamadas de sistema e o que elas fazem: tudo está na documentação. As páginas de manual estão com pressa em ajudar! Certifique-se de que o pacote esteja instalado antes de executar o comando man
man-pages
:
[root@sandbox tmp]# rpm -qa | grep -i man-pages
man-pages-3.53-5.el7.noarch
[root@sandbox tmp]#
Lembre-se de que você precisa adicionar "2" entre o comando
man
e o nome do syscall. Se você ler man
sobre man
( man man
), verá que a seção 2 é reservada para chamadas do sistema. Da mesma forma, se precisar de informações sobre as funções da biblioteca, você precisará adicionar 3 entre man
e o nome da função da biblioteca.
Abaixo estão os números das seções
man
:
1. .
2. (, ).
3. ( ).
4. ( /dev).
Para visualizar a documentação de uma chamada de sistema, execute man com o nome dessa chamada de sistema.
man 2 execve
De acordo com a documentação, uma chamada de sistema
execve
executa um programa que é passado a ela em parâmetros (neste caso é ls
). Parâmetros adicionais para ls também são passados para ele. Neste exemplo, é testdir
. Assim, esta chamada de sistema simplesmente funciona ls
com testdir
como um parâmetro:
'execve - execute program'
'DESCRIPTION
execve() executes the program pointed to by filename'
A próxima chamada do sistema
stat
recebe um parâmetro testdir
:
stat("testdir/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 0
Para ver o uso da documentação
man 2 stat
. A chamada do sistema stat retorna informações sobre o arquivo especificado. Lembre-se de que tudo no Linux é um arquivo, incluindo diretórios.
Em seguida, a chamada do sistema é
openat
aberta testdir
. Observe que o valor de retorno é 3. Este é o descritor de arquivo que será usado em chamadas de sistema subsequentes:
openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
Agora abra o arquivo
trace.log
e observe a linha após a chamada do sistema openat
. Você verá uma chamada de sistema getdents
que faz a maior parte do trabalho necessário para executar o comando ls testdir
. Agora vamos executar grep getdents
para o arquivo trace.log
:
[root@sandbox tmp]# grep getdents trace.log
getdents(3, /* 4 entries */, 32768) = 112
getdents(3, /* 0 entries */, 32768) = 0
[root@sandbox tmp]#
A documentação (
man getdents
) diz que getdents
lê as entradas do diretório, que é o que realmente precisamos. Observe que o argumento para getdent
3 é o descritor de arquivo obtido anteriormente na chamada do sistema openat
.
Agora que o conteúdo do diretório foi recebido, precisamos encontrar uma maneira de exibir as informações no terminal. Então, fazemos
grep
para outra chamada de sistema write
, que é usada para enviar para o terminal:
[root@sandbox tmp]# grep write trace.log
write(1, "file1 file2\n", 13) = 13
[root@sandbox tmp]#
Nos argumentos, você pode ver os nomes dos arquivos a serem produzidos:
file1
e file2
. Para o primeiro argumento (1), lembre-se de que no Linux, três descritores de arquivo são abertos por padrão para qualquer processo:
- 0 - fluxo de entrada padrão
- 1 - fluxo de saída padrão
- 2 - fluxo de erro padrão
Assim, a chamada do sistema
write
recebe file1
e file2
a saída padrão, que é um terminal, denota o número 1.
Agora que você sabe o que as chamadas do sistema fazem a maior parte do trabalho para a equipe
ls testdir/
. Mas e quanto às outras 100+ chamadas de sistema no arquivo trace.log
?
O sistema operacional faz várias coisas de suporte para iniciar um processo, portanto, muito do que você vê no arquivo
trace.log
é a inicialização e limpeza do processo. Dê uma olhada completa no arquivo trace.log e tente entender o que acontece quando o comando é executado ls
.
Agora você pode analisar chamadas de sistema para qualquer programa. O utilitário strace também fornece muitas opções de linha de comando úteis, algumas das quais são descritas a seguir.
Por padrão
strace
, ele não exibe todas as informações sobre as chamadas do sistema. No entanto, tem uma opção -v verbose
que mostrará informações adicionais sobre cada chamada do sistema:
strace -v ls testdir
É uma boa prática usar um parâmetro
-f
para controlar os processos filhos criados por um processo em execução:
strace -f ls testdir
Mas e se você quiser apenas os nomes das chamadas do sistema, o número de vezes que foram executadas e a porcentagem de tempo gasto na execução? Você pode usar a opção
-c
para obter estas estatísticas:
strace -c ls testdir/
Se quiser rastrear uma chamada de sistema específica, por exemplo,
open
e ignorar outras, você pode usar a opção -e
com o nome da chamada de sistema:
[root@sandbox tmp]# strace -e open ls testdir
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libcap.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libacl.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libattr.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
file1 file2
+++ exited with 0 +++
[root@sandbox tmp]#
E se você precisar filtrar por várias chamadas de sistema? Não se preocupe, você pode usar a mesma opção
-e
e separar as chamadas de sistema necessárias com uma vírgula. Por exemplo, para write
e getdent
:
[root@sandbox tmp]# strace -e write,getdents ls testdir
getdents(3, /* 4 entries */, 32768) = 112
getdents(3, /* 0 entries */, 32768) = 0
write(1, "file1 file2\n", 13file1 file2
) = 13
+++ exited with 0 +++
[root@sandbox tmp]#
Até agora, rastreamos apenas execuções de comandos explícitos. Mas e quanto aos comandos que foram executados anteriormente? E se você quiser rastrear demônios? Para isso, você
strace
tem uma opção especial -p
para a qual pode passar o ID do processo.
Não iniciaremos o daemon, mas usaremos um comando
cat
que exibe o conteúdo do arquivo passado a ele como um argumento. Mas se você não especificar um argumento, o comando cat
apenas aguardará a entrada do usuário. Depois de inserir o texto, ele exibirá o texto inserido na tela. E assim por diante até que o usuário clique Ctrl+C
para sair.
Execute o comando
cat
em um terminal.
[root@sandbox tmp]# cat
Em outro terminal, encontre o ID do processo (PID) com o comando
ps
:
[root@sandbox ~]# ps -ef | grep cat
root 22443 20164 0 14:19 pts/0 00:00:00 cat
root 22482 20300 0 14:20 pts/1 00:00:00 grep --color=auto cat
[root@sandbox ~]#
Agora comece
strace
com a opção -p
e o PID que você encontrou ps
. Depois de inicializado, strace
exibirá informações sobre o processo ao qual se conectou, bem como seu PID. Agora strace
monitora as chamadas do sistema feitas pelo comando cat
. A primeira chamada do sistema que você verá é lida, aguardando a entrada do thread 0, ou seja, da entrada padrão, que agora é o terminal em que o comando está sendo executado cat
:
[root@sandbox ~]# strace -p 22443
strace: Process 22443 attached
read(0,
Agora volte para o terminal onde você deixou o comando em execução
cat
e digite algum texto. Para demonstração, entrei x0x0
. Observe que eu cat
simplesmente repeti o que inseri e x0x0
a tela aparecerá duas vezes.
[root@sandbox tmp]# cat
x0x0
x0x0
Volte para o terminal onde você se
strace
conectou ao processo cat
. Agora você vê duas novas chamadas de sistema: a anterior read
, que agora foi lida x0x0
, e mais uma para gravação write
, que grava de x0x0
volta no terminal, e novamente uma nova read
, que está aguardando uma leitura do terminal. Observe que a entrada padrão (0) e a saída padrão (1) estão no mesmo terminal:
[root@sandbox ~]# strace -p 22443
strace: Process 22443 attached
read(0, "x0x0\n", 65536) = 5
write(1, "x0x0\n", 5) = 5
read(0,
Imagine os benefícios de lançar
strace
para daemons: você pode ver tudo o que está acontecendo em segundo plano. Complete o comandocat
Clicando Ctrl+C
... Isso também encerrará a sessãostrace
uma vez que o processo monitorado foi encerrado.
Para ver os carimbos de data / hora das chamadas do sistema, use a opção
-t
:
[root@sandbox ~]#strace -t ls testdir/
14:24:47 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
14:24:47 brk(NULL) = 0x1f07000
14:24:47 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2530bc8000
14:24:47 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
14:24:47 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
E se você quiser saber o tempo gasto entre as chamadas do sistema? Existe uma opção útil
-r
que mostra o tempo necessário para completar cada chamada do sistema. Muito útil, não é?
[root@sandbox ~]#strace -r ls testdir/
0.000000 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
0.000368 brk(NULL) = 0x1966000
0.000073 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb6b1155000
0.000047 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
0.000119 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
Conclusão
O utilitário é
strace
muito útil para aprender chamadas de sistema no Linux. Para outras opções de linha de comando, consulte man e a documentação online.
