Pular para o conteúdo

A Task State Segment

A Task State Segment é uma estrutura que o protected mode introduz brevemente como a fonte do stack pointer usado numa transição de ring 3 para ring 0, mas a TSS é uma estrutura maior do que esse único campo, e uma da qual praticamente todo kernel que suporta código em user mode acaba dependendo para mais do que apenas esse ponteiro. Diferente de uma entrada de GDT ou IDT, uma TSS não é consultada por índice em todo acesso; em vez disso, exatamente uma é marcada como atual de cada vez ao carregar seu seletor no registrador especial TR, e a CPU lê campos dessa TSS atual automaticamente sempre que eventos de hardware específicos, principalmente interrupções que elevam privilégio, ocorrem.

A TSS de 64 bits é uma estrutura fixa de 104 bytes (mais, opcionalmente, um bitmap de permissão de I/O anexado): três stack pointers (RSP0 a RSP2, um por nível de privilégio para o qual uma transição poderia levar vindo de um nível inferior), sete ponteiros de Interrupt Stack Table usados pelo mecanismo que o artigo da IDT já cobre, e um campo de offset de 16 bits apontando para onde o bitmap de permissão de I/O começa, relativo à própria base da TSS. A TSS de modo de 32 bits é anterior a tudo isso e é consideravelmente maior, já que também contém um conjunto completo de registradores salvos (EAX a EDI, seletores de segmento, EFLAGS) para o mecanismo de troca de tarefa por hardware ao redor do qual a estrutura foi originalmente projetada.

struct tss_64 {
uint32_t reserved0;
uint64_t rsp0, rsp1, rsp2;
uint64_t reserved1;
uint64_t ist1, ist2, ist3, ist4, ist5, ist6, ist7;
uint64_t reserved2;
uint16_t reserved3;
uint16_t iopb_offset;
} __attribute__((packed));

Os campos de troca de stack e transições de privilégio

Seção intitulada “Os campos de troca de stack e transições de privilégio”

RSP0 (ainda comumente chamado de ESP0/SS0, seu nome do modo de 32 bits, na maior parte do código-fonte de kernels) é o campo que importa para essencialmente todo kernel: sempre que um syscall, interrupção, ou exceção eleva o privilégio da CPU de ring 3 para ring 0, a CPU carrega RSP a partir desse campo antes de empilhar o frame de interrupção, porque uma stack de ring 3 não pode ser confiável para ter espaço suficiente, ou sequer estar mapeada, para o kernel empilhar dados nela com segurança. Sem um RSP0 válido, não há destino definido algum para esse empilhamento; uma transição tentada com a TSS não definida ou apontando para memória não mapeada falha imediatamente, antes de sequer uma instrução do próprio tratador rodar. RSP1 e RSP2 existem por simetria com ring 1 e ring 2, mas veem essencialmente nenhum uso prático, já que quase nenhum kernel define níveis de privilégio intermediários entre código de usuário e de kernel.

O bitmap para o qual iopb_offset aponta é o que determina se código de ring 3 pode executar IN/OUT diretamente contra uma dada porta sem falhar e cair no kernel primeiro: um bit por porta de I/O, com um bit definido significando “esta porta não é acessível”, lido no momento em que código de ring 3 tenta um acesso de porta. Um kernel que quer que código em user mode tenha acesso irrestrito a portas, o caso comum, aponta iopb_offset além do fim do limite real da TSS, o que a CPU trata como “nenhum bitmap presente” e nega todas as portas; um kernel que quer conceder a um processo específico acesso a portas específicas (um driver em user space lendo as portas de um dispositivo particular, por exemplo) em vez disso aloca o bitmap completo de 8 KB e limpa apenas os bits das portas que esse processo tem permissão de tocar. Essa é uma configuração por TSS, não diretamente por processo, então trocar quais permissões de I/O de qual processo estão ativas num design de uma TSS por núcleo significa reescrever os bits relevantes do bitmap a cada troca de contexto entre um processo com acesso elevado a portas e um sem, um custo que alguns kernels evitam mantendo I/O de porta inteiramente por trás de chamadas de sistema em vez de conceder acesso direto de ring 3 a qualquer momento.

TR é um único registrador, um por núcleo, então um kernel de multiprocessamento simétrico não pode compartilhar uma única TSS entre CPUs: cada núcleo precisa de sua própria TSS, sua própria entrada de GDT descrevendo essa TSS (ou seu próprio slot dedicado numa GDT compartilhada), e seu próprio carregamento via LTR realizado durante o bring-up daquele núcleo, antes que o núcleo possa receber uma interrupção com segurança. Errar isso de uma forma fácil de não notar durante testes com um único núcleo só aparece quando um segundo núcleo é ligado: dois núcleos compartilhando uma TSS acabam competindo para sobrescrever o mesmo campo RSP0, então qual escrita de qual núcleo aconteceu mais recentemente determina silenciosamente para onde a próxima transição de ring 0 do outro núcleo empilha seu frame de interrupção, corrompendo a stack de kernel desse núcleo de uma forma que parece, a partir do crash sozinho, não relacionada à causa real.

Um descritor de TSS na GDT tem 16 bytes em x86-64 em vez dos 8 usuais, já que precisa do endereço-base completo de 64 bits que uma TSS podendo estar em qualquer lugar de um espaço de endereçamento de 64 bits exige; um kernel que copia o layout de descritor de 8 bytes de 32 bits por hábito acaba corrompendo qualquer descritor que venha logo depois dele na tabela. LTR também tem uma restrição pontual que vale conhecer antes que cause uma falha confusa: o bit busy do descritor de TSS precisa estar limpo antes do carregamento, e o próprio LTR define esse bit como efeito colateral, então tentar LTR o mesmo seletor duas vezes sem uma troca de tarefa intermediária (o que, nas transições baseadas em interrupção cobertas acima, nunca acontece) falha na segunda tentativa.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Capítulo 8: o layout completo da TSS, o bitmap de permissão de I/O, e as instruções LTR/STR.