
Em Habré já
escreveu sobre interceptação de chamadas de sistema por meio de
ptrace
; Alexa escreveu sobre este post muito mais detalhado, que eu decidi traduzir.
Por onde começar
A comunicação entre o programa depurado e o depurador ocorre usando sinais. Isso complica muito as coisas já difíceis; por diversão, você pode ler
a seção BUGS do man ptrace
.
Há pelo menos duas maneiras diferentes de iniciar a depuração:
ptrace(PTRACE_TRACEME, 0, NULL, NULL)
tornará o pai do processo atual um depurador para ele. Nenhuma assistência é necessária dos pais; man
aconselha gentilmente: “Um processo provavelmente não deve fazer essa solicitação se seus pais não esperam rastreá-la.” (Em outro lugar do homem, você viu a frase “provavelmente não deveria” ?) Se o processo atual já tiver um depurador, a chamada falhará.ptrace(PTRACE_ATTACH, pid, NULL, NULL)
tornará o processo atual um depurador para o pid
. Se o pid
já tiver um depurador, a chamada falhará. SIGSTOP
enviado ao processo de depuração e não continuará funcionando até que o depurador o descongele.
Esses dois métodos são completamente independentes; Você pode usar um ou outro, mas não faz sentido combiná-los.
É importante observar que
PTRACE_ATTACH
não é instantâneo: depois que
ptrace(PTRACE_ATTACH)
é chamado, geralmente
waitpid(2)
é
PTRACE_ATTACH
para aguardar até que
PTRACE_ATTACH
"funcione".
Você pode iniciar o processo filho em depuração usando
PTRACE_TRACEME
seguinte maneira:
static void tracee(int argc, char **argv) { if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) die("child: ptrace(traceme) failed: %m"); if (raise(SIGSTOP)) die("child: raise(SIGSTOP) failed: %m"); execvp(argv[0], argv); die("tracee start failed: %m"); } static void tracer(pid_t pid) { int status = 0; if (waitpid(pid, &status, 0) < 0) die("waitpid failed: %m"); if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { kill(pid, SIGKILL); die("tracer: unexpected wait status: %x", status); } } void shim_ptrace(int argc, char **argv) { pid_t pid = fork(); if (pid < 0) die("couldn't fork: %m"); else if (pid == 0) tracee(argc, argv); else tracer(pid); die("should never be reached"); }
Sem uma chamada de
raise(SIGSTOP)
, pode ser que o
execvp(3)
seja executado antes que o processo pai esteja pronto para isso; e as ações do depurador (por exemplo, interceptando chamadas do sistema) não serão iniciadas no início do processo.
Quando a depuração é iniciada, cada
ptrace(PTRACE_SYSCALL, pid, NULL, NULL)
irá "descongelar" o processo depurado até a primeira entrada na chamada do sistema e depois até a chamada do sistema sair.
Montador telecinético
ptrace(PTRACE_SYSCALL)
não retorna
nenhuma informação ao depurador; ele simplesmente promete que o processo que está sendo depurado será interrompido duas vezes a cada chamada do sistema. Para obter informações sobre o que está acontecendo com o processo depurado - por exemplo, em qual sistema o sistema parou -, é necessário subir em uma cópia de seus registros armazenados pelo kernel em um
struct user
em um formato que depende da arquitetura específica. (Por exemplo, em x86_64, o número da chamada estará no campo
regs.orig_rax
, o primeiro parâmetro passado estará em
regs.rdi
, etc.) Alexa comenta: “parece que você está escrevendo código de montagem em C que funciona com os registros do processador remoto.”
Em vez da estrutura descrita em
sys/user.h
, pode ser mais conveniente usar as constantes de índice definidas em
sys/reg.h
:
#include <sys/reg.h> /* . */ long ptrace_syscall(pid_t pid) { #ifdef __x86_64__ return ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*ORIG_RAX); #else // ... #endif } /* . */ uintptr_t ptrace_argument(pid_t pid, int arg) { #ifdef __x86_64__ int reg = 0; switch (arg) { case 0: reg = RDI; break; case 1: reg = RSI; break; case 2: reg = RDX; break; case 3: reg = R10; break; case 4: reg = R8; break; case 5: reg = R9; break; } return ptrace(PTRACE_PEEKUSER, pid, sizeof(long) * reg, NULL); #else // ... #endif }
Nesse caso, duas paradas do processo depurado - na entrada da chamada do sistema e na saída dela - não diferem de forma alguma do ponto de vista do depurador; para que o próprio depurador se lembre do estado de cada um dos processos depurados: se houver vários, ninguém garante que um par de sinais de um processo chegue em uma linha.
Descendentes
Uma das opções do
ptrace
,
PTRACE_O_TRACECLONE
, garante que todos os filhos do processo depurado sejam depurados automaticamente quando
saírem do fork(2)
. Um ponto sutil adicional aqui é que os descendentes tomados para depuração se tornam "pseudo-filhos" do depurador, e o
waitpid
responderá não apenas à interrupção de "filhos imediatos", mas também para parar de depurar "pseudo-filhos". O homem alerta sobre isso:
“A configuração do sinalizador WCONTINUED ao chamar waitpid (2) não é recomendada: o estado“ continuado ”é por processo e o consumo pode confundir o verdadeiro pai do rastreado”. - ou seja, “Pseudo-filhos” têm dois pais que podem esperar que parem. Para o programador do depurador, isso significa que
waitpid(-1)
aguardará não apenas os filhos imediatos pararem, mas também qualquer processo de depuração.
Signals
(Conteúdo bônus de um tradutor: esta informação não está no artigo em inglês)Como já mencionado no início, a comunicação entre o programa depurado e o depurador ocorre usando sinais. Um processo recebe o
SIGSTOP
quando um depurador está conectado a ele e, em seguida, o
SIGTRAP
toda vez que algo interessante acontece no processo que está sendo depurado - por exemplo, uma chamada do sistema ou um sinal externo. O depurador, por sua vez, recebe o
SIGCHLD
cada vez que um dos processos depurados (não necessariamente o filho imediato) "congela" ou "congela".
ptrace(PTRACE_SYSCALL)
processo depurado é realizado chamando
ptrace(PTRACE_SYSCALL)
(antes do primeiro sinal ou chamada do sistema) ou
ptrace(PTRACE_CONT)
(antes do primeiro sinal). Quando os sinais
SIGSTOP/SIGCONT
também são usados para finalidades não relacionadas à depuração, podem surgir problemas com o
ptrace
: se o depurador "descongela" o processo depurado que recebeu o
SIGSTOP
, do lado de fora parecerá que o sinal foi ignorado; se o depurador não "descongelar" o processo que está sendo depurado, o
SIGCONT
externo não poderá "descongelar" o processo.
Agora, a parte divertida: o Linux proíbe os processos de
depuração , mas não impede a criação de loops quando um pai e um filho depuram um ao outro. Nesse caso, quando um dos processos recebe qualquer sinal externo, ele "congela" via
SIGTRAP
- o
SIGCHLD
enviado para o segundo processo e também "congela" via
SIGTRAP
. É impossível tirar esses "co-depuradores" do impasse enviando o
SIGCONT
de fora; a única maneira é matar (
SIGKILL
) a criança, e o pai sairá da depuração e "congelará". (Se você matar um pai, o filho morrerá com ele.) Se o filho
PTRACE_O_EXITKILL
opção
PTRACE_O_EXITKILL
, o pai depurado por ele também morrerá.
Agora você sabe como implementar um par de processos que, ao receber qualquer sinal, congelam para sempre e morrem apenas juntos. Por que isso pode ser necessário na prática, não vou explicar :-)