Desenvolvimento

13 jun, 2012

Um olhar minucioso nas Threads Linux: Threads Locais

Publicidade

Threads estão por toda parte. Mesmo agora, enquanto você navega nesta página, as threads estão envolvidas no processo. É muito provável que você esteja com mais de uma aba aberta no navegador e cada uma tenha pelo menos uma thread associada a ela. O servidor que abastece essa página executa várias threads, a fim de servir múltiplas conexões simultaneamente. Pode haver exemplos incontáveis de threads, mas vamos nos concentrar em uma execução específica. Ou seja, a implementação Linux de threads.

É difícil de acreditar que os antigos kernels Linux não suportavam threads. Em vez disso, todo o “threading” era realizado inteiramente no espaço do usuário por uma biblioteca pthread (POSIX thread) escolhida para o programa específico. Isso me faz lembrar a minha tentativa de implementar multitarefa em DOS quando eu estava na faculdade – possível, mas cheio de dor de cabeça.

Os kernels mais modernos, pelo contrário, possuem suporte completo para threads, que, do ponto de vista do kernel, são assim chamados “processos leves”. Eles são geralmente organizados em grupos de threads, que, por sua vez, representam processos como conhecemos. Na verdade, a função getpid libc (e chamada de sistema sys_getpid) retorna um identificador de um grupo de thread.

Deixe-me reiterar – a melhor explicação é uma explicação por meio de um exemplo. Neste artigo, vou cobrir o processo de criação de thread em Linux de 64 bits rodando em um PC usando FASM.

Clone, Fork, Exec…

Existem várias chamadas de sistemas envolvidas em manipulações de processos. A mais conhecida é sys_fork. Essa chamada de sistema “divide” um processo em execução em dois – pai e filho. Enquanto ambos continuam a execução da instrução imediatamente após a invocação sys_fork, eles têm PID diferente (ID do processo) ou, como agora sabemos – TGID diferente (ID de grupo de threads), bem como cada um recebe um valor de retorno diferente do sys_fork. O valor de retorno é um filho de TGID para o processo pai e 0 para o filho. Em caso de erro, fork retorna -1 e estabelece o erro adequadamente, enquanto sys_fork devolve um código de erro negativo.

Exec não retorna. Bem, ele formalmente possui um retorno do tipo int, mas receber um valor de retorno significa que a função falhou. A função Exec* libc ou a chamada de sistema sys_execve são usadas para lançar um novo processo. Por exemplo, se seu aplicativo tem que iniciar outro, mas você não quer ou não pode, por qualquer motivo, executar a função system(), então o seu aplicativo tem que fazer fork e o processo filho chama exec, sendo substituído na memória pelo novo processo. A execução do novo processo começa normalmente a partir do seu ponto de entrada.

Clone – esta é a função em que estamos interessados. Clone é um wrapper libc para a chamada de sistema do Linux sys_clone e é declarado no header sched.h como se segue:

int clone(int (*fn)(void*), void *child_stack, int flags, void *arg, ...);

Convido você a ler a página do manual para a função clone libc em http://linux.die.net/man/2/clone.

sys_clone

Nós não iremos lidar com a função clone aqui. Há lotes de bons recursos na Internet que oferecem bons exemplos para ele. Em vez disso, vamos examinar a chamada de sistema do Linux sys_clone.

Antes de mais nada, vamos dar uma olhada na definição de sys_clone em arch/x86/kernel/process.c:

long sys_clone(unsigned long clone_flags, unsigned long newsp,
void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)

Embora a definição pareça bastante complicada, na realidade, ela só precisa de clone_flags e newsp para ser especificada.

Mas há uma coisa estranha – ela não leva um ponteiro para a função thread como um parâmetro. Isso é normal – sys_clone apenas executa a ação sugerida pelo seu nome – ela clona os processos. Mas e quanto ao clone do libc? – você pode perguntar. Como já mencionei acima, clone do libc é um wrapper e o que ele faz além de chamar sys_clone é configurar o seu endereço de retorno no processo clonado para o endereço da função thread. Mas examinemos com mais detalhes.

clone_flags – este valor diz ao kernel sobre como queremos que o nosso processo seja clonado. No nosso caso, como queremos criar uma thread, em vez de um processo separado, devemos utilizar os seguintes valores or’ed:

CLONE_VM (0x100) – diz ao kernel para permitir que o processo original e o clone estejam no mesmo espaço de memória;

CLONE_FS (0x200) – ambos possuem a mesma informação de sistema de arquivos;

CLONE_FILES (0x400) – descritores de compartilhamento de arquivo;

CLONE_SIGHAND (0x800) – ambos os processos compartilham os mesmos manipuladores de sinal;

CLONE_THREAD (0x10000) – diz ao kernel que ambos os processos pertenceriam ao mesmo grupo de thread (sejam threads dentro do mesmo processo);

SIGCHLD (0x11) – isso não é uma flag, é o número do sinal SIGCHLD, que seria enviado para o processo original (thread) quando a thread é encerrada (usada por funções wait).

newsp – o valor do stack pointer para o processo clonado (nova thread). Esse valor pode ser NULL e nesse caso ambas as threads estarem usando a mesma pilha. No entanto, se a nova thread tenta escrever para a pilha, então, devido ao mecanismo de copy-on-write, ela recebe novas páginas de memória, deixando a pilha da thread original intocada.

Alocação de pilha

Devido ao fato de que na maioria dos casos você gostaria de alocar uma nova pilha para uma nova thread, não posso deixar de abordar esse aspecto neste artigo. Para facilitar as coisas, vamos implementar uma função pequena, que receberia o tamanho da pilha solicitada em bytes e retornaria um ponteiro para a região de memória alocada.

Nota importante:

Como o Linux segue a convenção de chamada AMD64 quando executado em 64 bits, parâmetros de função e os argumentos de chamada de sistema são passados por intermédio dos seguintes registos:
Chamada de função: argumentos 1 – 6 via RDI, RSI, RDX, RCX, R8, R9; argumentos adicionais são passados na pilha.
Chamada de sistema: argumentos  1 – 6 via RDI, RSI, RDX, R10, R8, R9; argumentos adicionais são passados na pilha.

Declaração C:

void* map_stack(unsigned long stack_size);

Implementação:
PROT_READ    = 1
PROT_WRITE  = 2
MAP_PRIVATE  = 0x002
MAP_ANON      = 0x020
MAP_GROWSDOWN  = 0x100
SYS_MMAP      = 9

map_stack
push rdi rsi rdx r10 r8 r9 ;Save registers
mov rsi, rdi ;Requested size
xor rdi, rdi ;Preferred address (may be NULL)
mov rdx, PROT_READ or PROT_WRITE ;Memory protection
mov r10, MAP_PRIVATE or MAP_ANON or MAP_GROWSDOWN ;Allocation attributes
xor r8, r8 ;File descriptor (-1)
dec r8
xor r9, r9 ;Offset - irrelevant, so 0
mov rax, SYS_MMAP ;Set system call number
syscall ;Execute system call
pop r9 r8 r10 rdx rsi rdi ;Restore registers
ret

Chamar essa função seria tão fácil como:

mov  rdi, size
call map_stack

Essa função retorna ou um código de erro negativo, tal como previsto pela sys_mmap ou o endereço da região de memória alocada. À medida que especificamos o atributo MAP_GROWSDOWN, o endereço obtido chama a atenção para o topo da região alocada em vez de apontar para a sua parte inferior, tornando-o perfeito para especificar como um stack pointer novo.

Criação de thread

Nesta seção, vamos implementar uma função create_thread trivial. Ela alocaria a pilha (de tamanho padrão = 0x1000 bytes) para uma nova thread, invocaria sys_clone e para a instrução call create_thread ou para a função de thread, dependendo do valor de retorno de sys_clone.

Declaração C:

long create_thread(void(*thread_func)(void*), void* param);

Como você pode ver, o tipo de retorno do thread_func é nulo, ao contrário da função real clone. Vou mostrar por que um pouco mais tarde.

Implementação:

create_thread:
mov r14, rdi ;Save the address of the thread_func
mov r15, rsi ;Save thread parameter
mov rdi, 0x1000 ;Requested stack size
call map_stack ;Allocate stack
mov rsi, rax ;Set newsp
mov rdi, CLONE_VM or CLONE_FS or CLONE_THREAD or CLONE_SIGHAND or SIGCHLD ;Set clone_flags
xor r10, r10 ;parent_tid
xor r8, r8 ;child_tid
xor r9, r9 ;regs
mov rax, SYS_CLONE
syscall ;Execute system call
or rax, 0 ;Check sys_clone return value
jnz .parent ;If not 0, then it is the ID of the new thread
push r14 ;Otherwise, set new return address (thread_func)
mov rdi, r15 ;Set argument for the thread_func
ret ;Return to thread_func
.parent:
ret ;Return to parent (main thread)

Saindo da thread

Todo mundo que já tenha procurado na Web por tutorial de programação Assembly para Linux está familiarizado com a chamada de sistema sys_exit. Em uma plataforma Intel de 64 bits, ela é chamada de número 60. No entanto, todos eles (tutoriais) erram o alvo. Embora sys_exit funcione perfeitamente com aplicativos simples de thread do tipo hellow-world, a situação é diferente com os multithreads. Em geral, sys_exit encerra a thread, não um processo, que, no caso de um processo com uma única thread, é definitivamente o suficiente, mas pode levar a artefatos estranhos (ou até mesmo zumbis) se, por exemplo, uma thread continua imprimindo para stdout depois de ter terminado a thread principal.

Agora, a explicação prometida sobre o tipo de retorno thread_func. No nosso caso (como na maioria), o thread_func não retorna usando a instrução ret. Ele simplesmente não pode, já que não há nenhum endereço de retorno na pilha e mesmo que você coloque um – voltar não encerraria a thread. Em vez disso, você deve implementar algo parecido com a função exit_thread.

Declaração C:

void exit_thread(long result);

Implementação:

SYS_EXIT = 60
exit_thread:
; Result is already in RDI
mov rax, SYS_EXIT ; Set system call number
syscall ; Execute system call

Saindo do processo

Sair do processo geralmente significa encerramento total do processo de execução. O Linux graciosamente nos dá uma chamada de sistema que encerra um grupo de threads (processo) – sys_exit_group (número de chamada 231). A função para encerrar o processo é tão simples como isto:

Declaração C:
void exit_process(long result);

Implementação:

SYS_EXIT_GROUP = 231
exit_process:
; Result is already in RDI
mov rax, SYS_EXIT_GROUP ; Set system call number
syscall ; Execute system call

Código fonte anexado

O código fonte anexado a este artigo (que pode ser encontrado aqui) contém um exemplo trivial do aplicativo que cria thread com o método descrito acima. Além disso, contém a lista de números de chamada da sistema, tanto para plataformas de 32 e de 64 bits.

Nota para Nerds:
O código em anexo é para fins demonstrativos e pode não conter elementos tão importantes como a verificação de erros etc.

Sistemas de 32 bits

Caso você decida converter o código acima para ser executado em sistemas 32 bits, isso é muito fácil. Em primeiro lugar – altere os nomes de registros para os de 32 bits apropriados.

A segunda coisa é lembrar como os parâmetros são passados para as chamadas de sistema em kernels de 32 bits. Eles ainda são transmitidos por meio de registros, mas são diferentes. Parâmetros de 1 a 5 são passados através de EBX, ECX, EDX ESI, EDI. O número de chamada do sistema é colocado como de costume em EAX, o mesmo registro é utilizado para armazenar o valor de retorno após a conclusão da chamada de sistema.

Terceiro – usar 0x80 int em vez da instrução syscall.

Quarto – lembre-se de mudar prólogos de função devido a uma convenção de chamada diferente. Enquanto sistemas de 64 bits usam AMD64 ABI, sistemas de 32 bits utilizam argumentos cdecl que passam na pilha por padrão.

Espero que este artigo tenha sido interessante e útil.

?

Texto original disponível em http://syprog.blogspot.com.br/2012/03/linux-threads-through-magnifier-local.html