A arquitetura de processadores Intel x86 define quatro níveis de execução privilegiada (anéis ou rings), numerados de 0 a 3. Idealmente, o código do núcleo do sistema operacional executa no Ring 0 (o mais privilegiado), os drivers em Ring 1 e Ring 2, e as aplicações do usuário em Ring 3 (o menos privilegiado). No entanto, o sistema Linux simplifiac este modelo, utilizando apenas os níveis 0 e 3, que correspondem respectivamente ao modo do núcleo (kernel mode) e ao modo do usuário (user mode). O processador distingue entre esses modos verificando o segmento apontado pelos registradores CS:EIP.
Em uma máquina x86 de 32 bits, o espaço de endereçamento virtual de um processo é de 4 GB. A Unidade de Gerenciamento de Memória (MMU) é responsável por traduzir endereços lógicos em endereços físicos. O espaço de memória é dividido da seguinte forma:
| Espaço do Núcleo (1 GB) | 0xC0000000 - 0xFFFFFFFF |
|-------------------------|-------------------------|
| Espaço do Usuário (3 GB) | 0x00000000 - 0xBFFFFFFF |
Em modo de usuário, o código só pode acessar endereços no intervalo 0x00000000 a 0xBFFFFFFF. Em modo de núcleo, o acesso é permitido a qualquer endereço, incluindo o espaço reservado ao núcleo acima de 0xC0000000.
O Mecanismo de Interrupções
A instrução int do processador aciona o mecanismo de interrupção, que inclui as chamadas de sistema. Quando uma interrupção é disparada, o processador salva automaticamente na pilha (stack) o estado atual da execução: o endereço de retorno (CS:EIP), a palavra de estado (flags) e o topo da pilha do modo do usuário (SS:ESP). Em seguida, ele carrega nos registradores o topo da pilha do modo do núcleo, sua palavra de estado e aponta CS:EIP para o ponto de entrada do tratador de interrupção apropriado (por exemplo, system_call para chamadas de sistema).
Dentro do tratador de interrupção, a rotina SAVE_ALL salva o restante do contexto dos registradores na pilha do núcleo. Ao finalizar, a sequência de restauração (restore_all seguida da instrução iret) é responsável por devolver os valores salvos dos registradores do usuário ao processador, permitindo que a execução do programa continue de onde parou.
Realizando uma Chamada de Sistema
As chamadas de sistema fornecem uma interface controlada e segura para que programas em modo de usuário solicitem serviços ao núcleo, como acesso a hardware, gerenciamento de processos e operações de E/S.
No Linux para x86 de 32 bits, a invocação padrão de uma chamada de sistema ocorre ao executar a instrução int 0x80. Esta instrução causa a transição para o modo de núcleo, onde a rotina system_call assume o controle. O processo em modo de usuário deve indicar qual serviço deseja. Isso é feito passando o número da chamada de sistema (syscall number) no registrador EAX. Os parâmetros adicionais, se houver, são passados em uma sequência específica de registradores: EBX, ECX, EDX, ESI, EDI, EBP. Caso os parâmetros excedam o número de registradores disponíveis, um deles pode ser usado como ponteiro para uma área de memória conttendo os dados.
Exemplo: Obtendo a Hora do Sistema
O número de chamada de sistema para time() é 13 (0x0D em hexadecimal). A forma mais comum é utilizar a biblioteca C (libc):
#include <stdio.h>
#include <time.h>
int main() {
time_t tempo_atual;
struct tm *info_tempo;
tempo_atual = time(NULL);
info_tempo = localtime(&tempo_atual);
printf("Data e Hora: %d/%d/%d %02d:%02d:%02d\n",
info_tempo->tm_mday, info_tempo->tm_mon + 1, info_tempo->tm_year + 1900,
info_tempo->tm_hour, info_tempo->tm_min, info_tempo->tm_sec);
return 0;
}
O mesmo resultado pode ser alcançado diretamente com assembly inline em C, explicitando o mecanismo da chamada de sistema:
#include <stdio.h>
#include <time.h>
int main() {
time_t valor_tempo;
struct tm *detalhes;
__asm__ volatile(
"movl $0, %%ebx\n\t" /* Parâmetro: ponteiro nulo (ignorado por time()) */
"movl $0x0D, %%eax\n\t" /* Número da syscall para time() */
"int $0x80\n\t" /* Dispara a interrupção 0x80 */
"movl %%eax, %0" /* Armazena o valor de retorno (epoch) */
: "=m" (valor_tempo)
:
: "%eax", "%ebx"
);
detalhes = localtime(&valor_tempo);
printf("Data e Hora: %d/%d/%d %02d:%02d:%02d\n",
detalhes->tm_mday, detalhes->tm_mon + 1, detalhes->tm_year + 1900,
detalhes->tm_hour, detalhes->tm_min, detalhes->tm_sec);
return 0;
}
Exemplo: Renomeando um Arquivo (Dois Parâmetros)
A chamada de sistema rename(), com o número 38 (0x26), recebe dois parâmetros: os caminhos do arquivo antigo e do novo.
Usando a libc:
#include <stdio.h>
#include <unistd.h>
int main() {
const char *caminho_antigo = "documento.txt";
const char *caminho_novo = "documento_final.txt";
if (rename(caminho_antigo, caminho_novo) == 0) {
printf("Arquivo renomeado com sucesso.\n");
} else {
perror("Falha ao renomear o arquivo");
}
return 0;
}
Implementando com assemb inline, onde EBX recebe o primeiro parâmetro e ECX o segundo:
#include <stdio.h>
int main() {
const char *nome_velho = "dados.csv";
const char *nome_novo = "backup_dados.csv";
int resultado_syscall;
__asm__ volatile(
"movl %1, %%ebx\n\t" /* Primeiro parâmetro: ponteiro para nome_velho */
"movl %2, %%ecx\n\t" /* Segundo parâmetro: ponteiro para nome_novo */
"movl $0x26, %%eax\n\t" /* Número da syscall para rename() */
"int $0x80\n\t"
"movl %%eax, %0" /* Captura o código de retorno */
: "=r" (resultado_syscall)
: "r" (nome_velho), "r" (nome_novo)
: "%eax", "%ebx", "%ecx", "memory"
);
if (resultado_syscall == 0) {
printf("Arquivo renomeado com sucesso.\n");
} else {
printf("Erro ao renomear o arquivo (código: %d).\n", resultado_syscall);
}
return 0;
}
Fluxo Interno no Núcleo
O vetor de interrupção 0x80 é associado ao ponto de entrada system_call durante a inicialização do núcleo, especificamente na função trap_init().
/* Dentro de trap_init(), no arquivo arch/x86/kernel/traps.c */
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
SYSCALL_VECTOR é definido como 0x80. Quando a instrução int 0x80 é executada, o fluxo é desviado para o código assembly de system_call, localizado em arch/x86/kernel/entry_32.S. Este código é crucial para entender o processo. O fluxo simplificado é:
- Entrada e Salvamento de Contexto: A instrução
SAVE_ALLsalva todos os registradores na pilha do núcleo. - Validação: O valor em
EAX(número da syscall) é comparado comNR_syscalls. Se for inválido, o tratamento é desviado parasyscall_badsys. - Dispatching: O código
call *sys_call_table(, %eax, 4)calcula o endereço do handler correto na tabelasys_call_table(cada entrada tem 4 bytes) e invoca a função do núcleo correspondente (ex:sys_time,sys_rename). - Salvamento do Retorno: O valor de retorno da função do núcleo (geralmente em
EAX) é salvo na pilha, na posição correspondente aoEAXoriginal. - Verificações e Escalonamento: Antes de restaurar o contexto, o núcleo verifica flags no
thread_infoda tarefa. Se houver trabalho pendente (sinais, necessidade de reescalonamento), o fluxo passa porsyscall_exit_work. Este é um ponto comum de escalonamento de processos, ondeschedule()pode ser chamado. - Restauração e Retorno: Finalmente,
restore_alle a instruçãoiretrestauram o contexto do usuário (registradores, pilha, flags) e retornam a execução ao programa em modo de usuário.
Este processo detalhado demonstra como um simples int 0x80 no código do usuário gera uma complexa sequência de operações controladas pelo núcleo, garantindo a segurança e a estabilidade do sistema.