MyToyOS: Cuidados com gcc e assembler entre modos de operação

No que concerne códigos “não hosted” (freestanding), se você é como eu, usa C para a maioria das funções mais complexas, deixando assembly para aquelas que seriam mais complicadas de implementar em C. Neste artigo quero mostrar alguns problema que pode enfrentar.

Em primeiro lugar, o GCC permite o desenvolvimento de funções que serão usadas no modo real, basta usar a opção -m16 na linha de comando. Nessa modalidade ele adiciona os prefixos 0x66 e 0x67 nas instruções, para que os offsets e operandos continuem sendo de 32 bits. Mas, isso pode ser problemático com saltos e retornos de funções. Tomemos como exemplo a seguinte função:

int f(int a, int b) { return a + b; }

O compilador gera algo assim, como pode ser observado via objdump:

00000000 <f>:
   0: 67 66 8b 44 24 08   mov  eax,DWORD PTR [esp+0x8]
   6: 67 66 03 44 24 04   add  eax,DWORD PTR [esp+0x4]
   c: 66 c3               ret    

Os prefixos 0x67 e 0x66 nas instruções mov e add estão ai porque, no modo real, os registradores default são as variantes de 16 bits (ax, no caso) e precisamos do prefixo 0x66 para usarmos EAX. O prefixo 0x67 está ai porque ESP está sendo usado como base no modo de endereçamento. Mas, o que diabos esse 0x66 está fazendo antes do RET?

Infelizmente o GCC continua pensando que está no modo protegido i386 e o empilhamento dos endereços de retorno continua sendo feito em 32 bits. Isso é evidente ao se olhar os offsets dos argumentos a e b da função. De acordo com o macete de usarmos uma estrutura para acesso ao stack frame, podemos escrever a rotina acima, em NASM, assim:

struc stkfrm
.retaddr  resw 1
.a        resd 1
.b        resd 1
endstruc

bits 16

global f
f:
  mov eax,[esp+stkfrm.a]
  add eax,[esp+stkfrm.b]
  ret

O NASM, por outro lado, vai gerar o código correto para o RET:

 1                           struc stkfrm
 2 00000000 <res 00000002>   .retaddr resw 1
 3 00000002 <res 00000004>   .a  resd 1
 4 00000006 <res 00000008>   .b  resd 2
 5                           endstruc
 6                           
 7                           bits 16
 8                           
 9                           f:
10 00000000 66678B442402       mov eax,[esp+stkfrm.a]
11 00000006 666703442406       add eax,[esp+stkfrm.b]
12 0000000C C3                 ret

Em primeiro lugar, o endereço de retorno empilhado, em modo real, é de 16 bits, para uma chamada near e isso é expresso pela reserva de uma WORD no topo da pilha. Assim, os acessos a pilha serão [esp+2] e [esp+6], respectivamente para a e b. E nada de 0x66 antes de RET!

Para testar essa chamada, eis um programinha simples, em modo real, que implementa um simples setor de boot:

bits 16

global _start
_start:
  jmp   0x7c0:_main
_main:
  cld
  mov   ax,cs
  mov   ds,ax
  mov   es,ax

  ; apaga a tela:
  mov   ax,3
  int   0x10

  ; x = f(2,1);
  push  dword 1
  push  dword 2
  call  f

  ; printf("2 + 1 = %#08x\n", x);
  lea   di,[value];
  call  dword2hex
  lea   si,[msg]
  call  puts

  ;; while (1);
.L1:
  hlt
  jmp   .L1

msg:
  db    '2 + 1 = 0x'
value:
  db    '00000000 (?).',13,10,0

hextbl:
  db    '0123456789abcdef'

; Como implementada pelo GCC
f:
  mov   eax,[esp+2]
  add   eax,[esp+6]
;  mov   eax,[esp+4]    ;; GCC implementa assim...
;  add   eax,[esp+8]
;  db    0x66
  ret

; ES:DI = ptr onde armazenar os chars
; AL  = byte
; ---
; Destrói AX,BX e CX.
; Avança DI
byte2hex:
  mov   bx,ax
  shr   bx,4
  and   bx,0x0f
  mov   cl,[hextbl+bx]
  mov   bx,ax
  and   bx,0x0f
  mov   ch,[hextbl+bx]
  mov   ax,cx
  stosw
  ret

; DS:DI = ptr onde armazenar os chars.
; EAX = dword
; ---
; Destrói EAX,BX,CX e EDX.
; Avança DI
%macro cvt_upper_byte 0
  ror eax,8
  mov edx,eax
  and eax,0xff
  call byte2hex
  mov eax,edx
%endmacro

dword2hex:
  cvt_upper_byte
  cvt_upper_byte
  cvt_upper_byte
  cvt_upper_byte
  ret

puts:
  lodsb
  test  al,al
  jz    .puts_exit
  mov   ah,0x0e
  int   0x10
  jmp   puts
.puts_exit:
  ret

  times 510-($-$$) db 0
  dw    0xaa55

Compile e execute com:

$ nasm -f bin boot.asm -o boot.bin
$ qemu-system-i386 -drive file=boot.bin,index=0,media=disk,format=raw

O resultado será este:

Mude a função f para que ela fique exatamente como codificada pelo GCC e verá que isso ai não funcionará mais. A prefixação 0x66 para RET faz com que a instrução tente recuperar EIP da pilha, só que apenas IP foi empilhado… Ao mesmo tempo, GCC supõe que o ambiente ainda seja de 32 bits e, por isso, ESP+4 e ESP+8, ao invés de ESP+2 e ESP+6, são usados para obter os dados da pilha…

Outro detalhe são as chamadas. Vejamos:

int g(int a, int b) { return f(a,b)+7; }

Isso criará um código assim:

00000010 <g>:
  10: 67 66 ff 74 24 08   push  DWORD PTR [esp+0x8]
  16: 67 66 ff 74 24 08   push  DWORD PTR [esp+0x8]
  1c: 66 e8 fc ff ff ff   call  1e <g+0xe>
  22: 66 5a               pop   edx
  24: 66 83 c0 07         add   eax,0x7
  28: 66 59               pop   ecx
  2a: 66 c3               ret

Note que a instrução CALL usa um deslocamento de 32 bits ao invés de 16. Isso não é problemático, graças ao prefixo 0x66 e ao fato de que todo salto near usa endereçamento relativo do EIP (ou IP). O único problema aqui é mesmo o RET prefixado.

Rotina em ASM chamando função em C e vice-versa

Para corrigir esse problema podemos, num código para o modo real escrito em assembly, simplesmente empilhar uma WORD 0 antes de chamar a função em C:

; Chamando função f(), em asm:
  push word 0
  call f
  ...

Isso garante que, na pilha, teremos IP estendido com zeros, formando o offset de 16 bits dentro do segmento de código. Podemos até mesmo criar uma macro:

%macro pm_call 1
  push word 0
  call %1
%endmacro

Do outro lado, a chamada de uma função feita para o modo real à partir do código em C gerado pelo GCC, espera que uma DWORD seja desempilhada no RET, já que CALL, prefixado com 0x66, ira colocar EIP na pilha. Podemos copiar a WORD apontado por ESP para ESP+2 e adicionar 2 a ESP, na rotina em ASM… Lembre-se que EIP é a última coisa empilhada antes do salto:

%macro pm_ret 0
  push ax
  mov  ax,[esp+2]
  mov  [esp+4],ax
  pop  ax
  add  esp,2   ; Descarta a primeira WORD do topo da pilha.
  ret
%endmacro

Ok, é gambiarra, mas, infelizmente o GCC não gera código muito bom em 16 bits!

Uma outra forma de explicar ponto flutuante…

Já expliquei de várias formas aqui, ai vai mais uma: “Ponto flutuante é uma maneira de lidar com frações racionais”. Para entender isso, voltemos à equação que descreve um float:

\displaystyle v=(-1)^S+\left(1+\frac{F}{2^{23}} \right )\cdot2^e

Onde os valores S, F e e são os “pedaços” de um float e são inteiros e positivos:

Estrutura de um float

Note que S tem apenas 1 bit de tamanho (S de “sinal”), F (de “fração”) tem 23 bits e, portando, suporta valores entre 0 e 2^{23}-1. Assim, \frac{F}{2^{23}} sempre será menor que 1.0! Já e é expresso pela equação e=E-127. O E, maiúsculo, é o valor que consta na estrutura…

Sendo assim, 1+\frac{F}{2^{23}} sempre será uma fração racional. Isso é evidente se você pensar que, numa estrutura com quantidade limitada de bits, não dá para armazenar um valor como π (3.1415926… e não acaba nunca).

O importante aqui é lembrar que a fração racional deve ser racional em binário. O valor 0.1, em decimal, não é racional em binário. Aliás, qualquer valor que seja diferente de 0.0 que não termine, na parte fracionária, em 5, não é um valor racional em binário. Isso é fácil perceber quando você calcula cada “casa” binária “depois da vírgula”:

2^{-1}=0.5\quad2^{-2}=0.25\quad2^{-3}=0.125\quad\cdots

E se somarmos cada um desses bits “fracionários”, verá que o final sempre terá o algarismo 5, em decimal.

Regra geral: Se a parte fracionária de um valor decimal for diferente de zero e não terminar em 5, o valor em ponto flutuante não é exato! Exemplo: 454.76 não é exato com qualquer tipo de ponto flutuante (float, double, long double) porque a parte fracionária termina em 6.

Localização em C

Alguém me perguntou sobre locale em C. Pra você que não sabe o que locale significa, é somente a maneira como algumas strings relacionadas a formatação se apresentam de acordo com a localidade onde o usuário se encontra. Por exemplo, no Brasil usamos ‘,’ (vírgula) ao invés de ‘.’ (ponto) decimal, quando queremos expressar valores fracionários, como em 3,141593. Mas, parece, que as rotinas das bibliotecas lidam apenas com o formato americano, usando o ‘.’. Bem… não é bem assim.

O padrão ISO 9899, que rege a linguagem C e a biblioteca padrão, nos fornece o header locale.h com estruturas e funções para lidarmos com localização. A localização default é chamada, simplesmente, de “C” e segue o padrão americano. Mas, podemos ajustar a localidade para a nossa língua, nosso país e o charset que usamos, desde que o sistema operacional o tenha acessível. Para isso usamos a função setlocale(), que tem o protótipo:

char *setlocale(int category, const char *locale);

A “categoria” é uma das constantes LC_ALL, LC_ADDRESS, LC_COLLATE, LC_CTYPE, LC_IDENTIFICATION, LC_MEASUREMENT, LC_MESSAGES, LC_MONETARY, LC_NAME, LC_NUMERIC, LC_PAPER, LC_TELEPHONE ou LC_TIME. Cada uma dessas características endereça um aspecto da sua localidade… LC_NUMERIC, por exemplo, é usada para ajustar como o “ponto decimal” e o “separador de milhar” serão usados. LC_MONETARY, a mesma coisa, mas também onde a string da moeda será colocada e se como valores negativos são expressos… A categoria LC_CTYPE tem a ver com as macros contidas no header ctype.h. E por ai vai…

O argumento “locale” é uma string com um formato específico. Pro Brasil, por exemplo, pode-se usar “pt_BR” ou “pt_BR.utf8” (no caso do Linux/BSD/MacOS). O formato segue o padrão “língua[_país][.charset]”, onde “língua” segue a abreviação especificada na ISO 639, enquanto “país” segue a especificação ISO 3166… Mas, é importante notar que apenas os locales disponíveis no sistema operacional são aceitáveis e eles podem ser obtidos assim:

$ locale -a
C
C.UTF-8
en_AG
en_AG.utf8
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_IN.utf8
en_NG
en_NG.utf8
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZM
en_ZM.utf8
en_ZW.utf8
POSIX
pt_BR.utf8

Note que, no meu caso, “pt_BR” não está disponível, apenas “pt_BR.utf8”.

O programinha abaixo mostra locale em funcionamento:

#include <stdio.h>
#include <locale.h>

void main(void)
{
  float f;

  setlocale(LC_ALL, "pt_BR.utf8");
  fputs("Entre com um valor float (usando ',' como separador decimal): ", stdout);
  fflush(stdout);
  scanf("%f", &f);
  printf("Valor: %f\n", f);
}

Compilando e executando:

$ cc -o test test.c
$ ./test
Entre com um valor float (usando ',' como separador decimal): 3,14
Valor: 3,140000

Tanto o scanf() quanto o printf() obedeceram a localização ajustada por setlocale()! Mas, note, não é que essas funções respeitem a localização, mas as funções que elas usam o fazem (atof(), por exemplo). Outro ponto interessante é sobre os “separadores de milhar”. Eles não são respeitados nessas funções! Faça float f = atof("32.768,9"); com o locale pt_BR.utf8 ativo e você, provavelmente, obterá 0.0f como resposta, indicando falha da conversão. Não espere que scanf() e printf() vão entender valores monetários, por exemplo… O locale LC_MONETARY existe somente para que você possa pegar as regras correspondentes na estrutura lconv.

Para saber quais são as regras do locale selecionado, use a função localeconv() que retorna um ponteiro para uma estrutura lconv contendo um monte de informações (disponiveis na especificação da linguagem C, aqui). Você não deve modificar os valores da estrutura…

Uma maneira simples de livrar-se dos separadores de milhar, levando-se em conta o indicado em lconv, é usando uma rotina como essa aqui:

#include <string.h>
#include <locale.h>

char *strip_thousands_sep(char *s)
{
  struct lconv *pl;

  pl = localeconv();
  if (*pl->mon_thousands_sep)
  {
    char *p = s;

    while (p = strchr(p, *pl->mon_thousands_sep))
      strcpy(p, p + 1);  // Não há problema em fazer
                         // essa cópia. Não há overlapping
                         // aqui e ambos os ponteiros
                         // são válidos.
  }

  return s;
}

É claro, a localidade terá que ser ajustada antes de chamar essa rotina. E, como já dito, para retornar a localidade default, basta usar “C”, ao invés de “pt_BR.utf8”.

A localização também é útil para comparação de strings. A função strcoll() leva em consideração a localidade para comparar duas strings. O exemplo contido no site cppreference (exemplo, aqui) ilustra bem. Note que strcmp() e derivadas não lidarão com localização… Outra função para lidar com strings e localização útil é strxfrm(). Ela transforma (daí a abreviação xfrm) uma string numa representação que pode ser usada em strcmp(), ou derivadas, para ter o mesmo efeito de strcoll(). Isso pode ser útil em línguas como hebraico e árabe, que são escritas “de trás para frente”…

Instalando novas localidades no Linux:

Nada mais simples. Linux disponibiliza pacotes para linguagens específicas. Para listá-las todas (Debian/Ubuntu):

$ apt-cache search language-pack | grep 'pack-.. '
language-pack-af - translation updates for language Afrikaans
language-pack-am - translation updates for language Amharic
language-pack-an - translation updates for language Aragonese
language-pack-ar - translation updates for language Arabic
language-pack-as - translation updates for language Assamese
language-pack-az - translation updates for language Azerbaijani
language-pack-be - translation updates for language Belarusian
language-pack-bg - translation updates for language Bulgarian
language-pack-bn - translation updates for language Bengali
language-pack-bo - translation updates for language Tibetan
language-pack-br - translation updates for language Breton
language-pack-bs - translation updates for language Bosnian
language-pack-ca - translation updates for language Catalan; Valencian
language-pack-cs - translation updates for language Czech
language-pack-cy - translation updates for language Welsh
language-pack-da - translation updates for language Danish
language-pack-de - translation updates for language German
language-pack-dz - translation updates for language Dzongkha
language-pack-el - translation updates for language Greek, Modern (1453-)
language-pack-en - translation updates for language English
language-pack-eo - translation updates for language Esperanto
language-pack-es - translation updates for language Spanish; Castilian
language-pack-et - translation updates for language Estonian
language-pack-eu - translation updates for language Basque
language-pack-fa - translation updates for language Persian
language-pack-fi - translation updates for language Finnish
language-pack-fr - translation updates for language French
language-pack-ga - translation updates for language Irish
language-pack-gd - translation updates for language Gaelic; Scottish Gaelic
language-pack-gl - translation updates for language Galician
language-pack-gu - translation updates for language Gujarati
language-pack-he - translation updates for language Hebrew
language-pack-hi - translation updates for language Hindi
language-pack-hr - translation updates for language Croatian
language-pack-hu - translation updates for language Hungarian
language-pack-ia - translation updates for language Interlingua (International Auxiliary Language Association)
language-pack-id - translation updates for language Indonesian
language-pack-is - translation updates for language Icelandic
language-pack-it - translation updates for language Italian
language-pack-ja - translation updates for language Japanese
language-pack-ka - translation updates for language Georgian
language-pack-kk - translation updates for language Kazakh
language-pack-km - translation updates for language Central Khmer
language-pack-kn - translation updates for language Kannada
language-pack-ko - translation updates for language Korean
language-pack-ku - translation updates for language Kurdish
language-pack-lt - translation updates for language Lithuanian
language-pack-lv - translation updates for language Latvian
language-pack-mk - translation updates for language Macedonian
language-pack-ml - translation updates for language Malayalam
language-pack-mn - translation updates for language Mongolian
language-pack-mr - translation updates for language Marathi
language-pack-ms - translation updates for language Malay
language-pack-my - translation updates for language Burmese
language-pack-nb - translation updates for language Bokmål, Norwegian; Norwegian Bokmål
language-pack-ne - translation updates for language Nepali
language-pack-nl - translation updates for language Dutch; Flemish
language-pack-nn - translation updates for language Norwegian Nynorsk; Nynorsk, Norwegian
language-pack-oc - translation updates for language Occitan (post 1500)
language-pack-or - translation updates for language Oriya
language-pack-pa - translation updates for language Panjabi; Punjabi
language-pack-pl - translation updates for language Polish
language-pack-pt - translation updates for language Portuguese
language-pack-ro - translation updates for language Romanian
language-pack-ru - translation updates for language Russian
language-pack-si - translation updates for language Sinhala; Sinhalese
language-pack-sk - translation updates for language Slovak
language-pack-sl - translation updates for language Slovenian
language-pack-sq - translation updates for language Albanian
language-pack-sr - translation updates for language Serbian
language-pack-sv - translation updates for language Swedish
language-pack-ta - translation updates for language Tamil
language-pack-te - translation updates for language Telugu
language-pack-tg - translation updates for language Tajik
language-pack-th - translation updates for language Thai
language-pack-tr - translation updates for language Turkish
language-pack-ug - translation updates for language Uighur; Uyghur
language-pack-uk - translation updates for language Ukrainian
language-pack-uz - translation updates for language Uzbek
language-pack-vi - translation updates for language Vietnamese
language-pack-xh - translation updates for language Xhosa

Se adicionar language-pack-ja, por exemplo, a localidade jp_JP.utf8 será adicionada nas tabelas de localidade do sistema e ficará disponível para uso de seus programas.

Provavelmente seu professor não explicou isso direito pra você! (parte 2)

Neste outro artigo, aqui, eu te expliquei o funcionamento do stream stdout e do uso de fflush(). Mas, deixei alguns detalhes de fora, especialmente quanto ao stream stdin.

Em um grupo do Facebook tenho visto uma galera usando fflush(stdin) em resoluções de exercícios, usando linguagem C. Essa construção é inútil porque, por padrão, fflush() funciona apenas para streams que podem ser escritas. Este é o caso de stdout.

Outro detalhe é sobre o uso da função scanf(). Ela tem uns problemas que podem ser desesperadores para o novato.

Também têm um valor de retorno. A função devolve um valor do tipo int que é o número de itens que puderam ser convertidos na “varredura” (scan) ou -1, em caso de erro catastrófico. Se você fizer:

int a, b, n;

n = scanf("%d %d", &a, &b);

E entrar com duas strings não numéricas, vai obter n diferente de 2. Se entrar com “xpto xpto”, obterá n=0. Se entrar com “1 xpto”, obterá n=1… A função também interpreta ‘\n’ como “espaço” a ser ignorado. Se você entrar com apenas “1”, a função continuará esperando pelo segundo argumento a ser convertido!

O termo “varredura” é importante! A função procura pelos itens a serem convertidos, de acordo com o formato especificado, varrendo o stream stdin, até conseguir convertê-los ou falhar, ignorando os “espaços”. No exemplo acima, se entrássemos com:

"1          1" ou "       1 1"

Teríamos a e b convertidos corretamente.

No caso de itens que não podem ser convertidos, aparece um problema: Eles permanecem no buffer de stdin! Para demonstrar isso, eis um exemplo:

#include <stdio.h>

void main(void)
{
  int a, b, n;

  for (;;) {
    printf("Entre com dois números: "); fflush(stdout);
    n = scanf("%d %d", &a, &b);
    printf("%d valores convertidos: %d e %d\n", n, a, b);
  }
}

Compile e execute esse programinha e entre com “abc abc”… Observe os printf‘s sendo executados um monte de vezes! Isso acontece porque scanf() deixará no buffer de stdin os caracteres não convertidos da string lida anteriormente (como expliquei ai em cima), bem como qualquer caracter “não pulado”. Assim, scanf() tentará reconvertê-los, falhando diversas vezes até limpar todo o buffer de stdin.

Infelizmente não há jeito portável de fazer um flushing no stream stdin… Mas, usando uma extensão da glibc podemos corrigir esse problema facilmente:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  char *line;
  size_t line_size;
  int n;

  unsigned char b;
  char str[21];

  do {
    printf("Entre com um valor de 8 bits e uma string separados por espaço:\n");

    // força alocação da linha em getline().
    line_size = 0;
    line = NULL;

    // Lê uma linha de stdin.
    // getline() é uma extensão da glibc!
    if (getline(&line, &line_size, stdin) == -1)
    {
      fputs("ERRO alocando espaço para conter a linha!\n", stderr);
      exit(EXIT_FAILURE);
    }

    // Para termos um valor definido, em caso de falha.
    str[0] = b = 0;

    // Usa sscanf() porque quando scanf falha, deixa o stream stdin com lixo!
    n = sscanf(line, "%hhu %20s", &b, str);

    // livra-se da linha alocada.
    free(line);

    printf("%d itens convertidos: %hhu, \"%s\"\n", n, b, str);
  } while (1);
}

Aqui, sscanf uão usa um stream para obter os valores convertidos, ela usa uma string! A função getline() lerá uma linha da entrada (stdin) — ou seja, até char um ‘\n’, que será colocado no final do buffer alocado, precedendo o caracter NUL — e retornará com buffer apontado por line dinamicamente alocado (ou retorna -1 em caso de falha). Note que depois usar o sscanf fiz uma chamada a free(), livrando-se do buffer alocado por getline.

Se você digitar ENTER e obter uma linha vazia, ela terá 1 único caracter (‘\n’) o o NUL. Assim, sscanf falhará, mas não há nenhum buffer mantido por um stream para bagunçar o coreto.

Já que getline() é uma extensão, você pode optar por usar fgets(), mas perderá o recurso de alocação dinâmica automática, tendo de codificar isso por si mesmo… Ahhh… não use gets() para ler o stream stdin. Essa função é obsoleta desde a especificação C99 e não deve ser mais usada.

TARDIS!

Time and Relative Dimensions In Space“! Yep! Tornei-me um “Whovian”! Mas, não vou falar da série britânica aqui, e sim dos CACHES e dois conceitos fundamentais: Localidade Temporal e Localidade Espacial.

Você pode achar estranho que o “Continuum” (espaço-tempo), conceito aplicável à Física e às séries de Ficção Científica, seja aplicável também à programação. Seu processador também usa conceito semelhante no que se refere aos CACHES. Lembre-se que CACHE é uma memória intermediária entre aquela endereçável nos seus pentes de memória (e na ROM) e a acessada, internamente, pelo processador, através de uma instrução. No meio do caminho temos algumas memórias RAM, rápidas, que são divididas em “linhas”, onde cada linha tem, hoje em dia, 64 bytes de tamanho.

Isso significa que o processador carregará, da memória principal, 64 bytes para dentro do CACHE e passará a lidar com esses 64 bytes, ao invés de, toda vez que quiser ler/escrever dados na memória, torrar ciclos de clock “esperando” que o circuito de sua RAM (por exemplo) esteja pronto para receber/enviar dados… Acontece que cada um dos CACHES disponíveis no interior do processador (e os disponíveis fora dele, como o cache L3, hoje em dia) tem capacidade limitada… Estamos interessados no cache L1 (sempre!), quando falamos de performance e este tem, no máximo 64 KiB de tamanho…

Sendo o cache L1 é o mais curto dos caches, tendo apenas 1024 linhas, precisamos nos certificar que todas as linhas disponíveis estejam sendo usadas pelo maior tempo possível. Isso significa que, se todas as linhas estiverem sendo usadas, para acessar memória que não consta do cache, uma linha terá que ser escrita na memória física, externa, e outro bloco de 64 bytes terá que ser lido e colocado no lugar. Isso também significa que os seus dados têm localidade temporal, ou seja, as linhas dos caches podem não estar presente nele por muito tempo.

O macete para obter alta performance é manter as linhas mais acessadas no cache o maior tempo possível, evitando poluí-lo com trocas ou “despejos” de linhas desnecessários… Nem sempre isso é possível, já que o kernel também usa o cache e não temos nenhum controle sobre o que o kernel faz, do ponto de vista de nossos códigos no userspace.

Existe também o conceito de localidade espacial. Se uma linha tem 64 bytes de tamanho, então, se mantivermos os dados mais acessíveis o mais próximos possível, poderíamos garantir que eles estejam na mesma linha, ou “no mesmo espaço”. Isso também vale para as instruções sendo executadas pelo processador… Se mantivermos um loop dentro de uma única linha, não corremos o risco de outra linha ser usada para conter as instruções do loop e ser “despejada”, precisar ser recarregada em cada iteração do loop, desperdiçando ciclos de clock! Por esse motivo, quanto menos instruções usarmos, melhor. A mesma coisa vale para o tamanho das instruções…

O mesmo raciocínio pode ser feito com dados. Se uma estrutura tiver 64 bytes ou menos de tamanho, podemos garantir que ela esteja dentro de uma única linha de cache desde que não ultrapasse a fronteira de uma linha. Como podemos fazer isso? Já que toda linha tem 64 bytes de tamanho, o endereço linear que marca o início de uma linha sempre terá os 6 bits inferiores zerados e a linha terminará com o endereço contendo os 6 bits inferiores setados… Note que um dado em um endereço linear não necessariamente está localizado no cache. Ele só estará lá quando for acessado (mostro outro jeito mais adiante).

Eis um exemplo de um array de 64 bytes alinhado com o início de uma linha de cache:

  section .data

  global array
  align 64  ; Alinha com o início de uma
            ; linha de cache.
array:
  times 64 db 0

Aqui, é garantido que o array inteiro, de 64 bytes, estará alocado em uma única linha do cache L1 quando for acessado. No entanto, isso não é lá muito prático… Em primeiro lugar, se ficarmos alinhando tudo quanto é estruturas no início de uma linha termos muitos “buracos” em nosso segmento de dados. De fato, poderemos até mesmo ter mais “buracos” do que dados efetivos. Garantiremos que uma estrutura estrutura individual qualquer esteja toda no menor número de linhas possível, mas nosso segmento de dados poderá ficar enorme!

Em segundo lugar, o principio da localidade espacial nos indica que devemos agrupar a maior quantidade de dados possível dentro de uma única linha… Ou seja, evitarmos que seus dados cruzem a fronteira de uma linha e passem a pertencer a duas linhas… Estatisticamente, se alinharmos nossos dados de 16 em 16 bytes (\frac{1}{4} de linha), podemos conseguir esse efeito com a maioria dos dados. No entanto, pode ser prudente o alinhamento de 8 ou 4 bytes, de acordo com o tipo de dado. No exemplo anterior, o array tem 64 bytes, então faz todo sentido alinhá-lo com o início de uma linha do cache se seu acesso for crítico para performance…

Mas, e quanto ao princípio de localidade temporal? Bem… para não poluir o cache, evitando “despejos”, podemos, às vezes, usar instruções especiais que lêem/escrevem direto na memória. MOVNTA, por exemplo, é uma instrução SSE que faz o MOV “Não temporal”, ou seja, não passa pelo cache… Às vezes é útil, mas não é prudente abusar… Acessar memória diretamente implica em esperar pela leitura/escrita e isso pode ser lento! Na prática queremos usar MOVNTA apenas para evitar despejos desnecessários de pequenos blocos de dados.

Para retardar a temporalidade de uma linha, podemos fazer um prefetch, ou seja, pedir ao processador que carregue uma linha antecipadamente, com os seus 64 bytes, se eles já não estiverem no cache. Para isso existe a instrução PREFETCH, que toma um endereço como argumento. Qualquer endereço no interior da linha fará com que a linha inteira seja carregada para o cache… Mas, note… Existem 3 caches diferentes no seu processador: O L1, L2 e L3… Existem 3 instruções PREFETCH também: PREFETCHT0, PREFETCHT1, PREFETCHT2 que, em teoria, pede para o processador transfira uma linha de um cache para outro ou da memória para um dos caches… Existe também PREFETCHW, que faz a mesma coisa que o PREFETCHT0, mas prepara a linha para a escrita (apenas as linhas que foram “modificadas” são reescritas num cache de nível superior ou na memória do sistema – PREFETCHW, suponho, marca a linha como “suja”, forçando a escrita quando ela for “despejada”).

As instruções de prefetching são uma “dica” para o processador. Usar essas instruções não garante que as linhas do cache sejam preenchidas. Mas, pelo menos, informa a intenção e pode melhorar a performance um bocado… Quando você precisar forçar um prefetching, recomendo o uso de PREFETCHT0, que tentará carregar todos os caches com a linha correspondente…

Essas instruções são úteis se você tem uma grande quantidade de dados e quer organizar quais porções deverão estar presentes no cache. Por exemplo, suponha que você tenha duas matrizes 4×4, onde cada item seja do tipo float. Temos 16 floats e, portanto 64 bytes que, é claro, cabe exatamente numa linha!… Você pode querer colocar cada uma das matrizes no cache L1d antes de começar a manipulá-los… Suponha que queira multiplicar uma matriz por outra e obter uma terceira… Suponha também que as três matrizes estejam alinhadas com o início de uma linha de cache (endereço com os 6 bits inferiores zerados)… Antes de multiplicá-las você poderia fazer:

; Estou assumindo que mat1, mat2 e
; mat_result estejam alinhados com o início 
; de uma linha.
  ...
  ; faz o prefetching...
  prefetcht0 [mat1]
  prefetcht0 [mat2]
  prefetcht0 [mat_result]

  ; chama a função matrix_mult.
  lea rdi,[mat_result]
  lea rsi,[mat1]
  lea rdx,[mat2]
  call matrix_mult
  ...

Com os 3 primeiros prefetches você dá a dica ao processador de que precisa dos 3 blocos de dados nos caches. Especialmente no cache L1! Claro, se matrix_mult “consumir muita memória”, esses prefetches serão “perdidos” (temporalidade!), mas aqui estamos usando apenas 192 bytes, ou 3 linhas do cache L1, que tem 1024…

Aí está! Espaço e Tempo são importantes quando lidamos com caches.

MyToyOS: PCI e PCI Express

Já falei sobre PCI aqui neste artigo. Embora existam alguns pequeno errinhos de interpretação de minha parte nele, o artigo é, essencialmente, correto. Aqui eu vou corrigir esses errinhos e mostrar como usar o configuration space direito. Outra coisa que falarei é sobre as modificações do padrão PCI Express.

Em primeiro lugar, não é que PCI oferece acesso de I/O mapeado em memória apenas. Ele oferece isso preferencialmente. A especificação nos diz que o espaço de endereçamento de I/O (portas) são usados, tipicamente, para dispositivos legados ou, como está dito lá, “espalhados” (onde, apenas o endereço base é fornecido)… O header no configuration space nos dá, pelo menos, dois BARs (Base Address Registers), que são os registradores com os endereços base do dispositivo, codificados.

A outra coisa, que pode não ter ficado perfeitamente clara no artigo anterior, é que existem 3 “espaços” de endereçamento: Memória, I/O (portas) e Configuração… A ideia do PCI não é apenas fornecer um barramento de alta velocidade entre o processador e o dispositivo, mas também uma maneira padronizada de “perguntar” aos dispositivos como podemos acessá-los. Para isso este espaço adicional (o de Configuração) existe. No padrão PCI ele é acessado pelas portas 0xCF8 e 0xCFC, escrevendo e lendo DWORDs sempre… A tentativa de escrever ou ler outra coisa que não DWORDs não acessará o configuration space.

No PCI, o configuration space fornece blocos de 256 bytes (64 DWORDs) contendo significados fixos:

Configuration space

Essa estrutura aplica-se ao barramento PCI local e apenas se estamos lidando com um dispositivo que não seja uma PCI-to-PCI bridge. Mesmo assim, os 24 bytes iniciais aplicam-se a todos os dispositivos…

No offset 0 temos o VendorID e o DeviceID, onde VendorID é usado como condição para a existência do dispositivo. Se você obtiver um VendorID igual a -1 (ou 0xffff), não tem um dispositivo pendurado no barramento, de acordo com o configuration space. O DeviceID, claro, identifica o dispositivo, assim como o VendorID identifica o fabricante (0x8086, por exemplo, é “Intel”).

As rotinas abaixo, escritas para MS-DOS, usando o DJGPP, demonstra a varredura de todos os dispositivos conectados ao barramento PCI:

/* pci.c */
#include <pc.h>
#include <stdint.h>
#include "pci.h"

#ifndef __DJGPP__
#error Isso só compila no DJGPP!
#endif

/* Lê um DWORD do configuration space. */
__attribute__((regparm(3)))
uint32_t pciGetConfigLong(uint8_t bus,
                          uint8_t device,
                          uint8_t function,
                          uint8_t offset)
{
  uint32_t data;
  outportl(0xCF8, 
           PCI_CONFIG_ADDR(bus, 
                           device, 
                           function, 
                           offset));
  return inportl(0xCFC);
}

/* Retorna VendorID e DeviceID 
   (se deviceIDptr != NULL). */
__attribute__((regparm(3)))
uint16_t pciGetConfigVendorID(uint8_t bus, 
                              uint8_t device, 
                              uint8_t function, 
                              uint16_t *deviceIDptr)
{
  uint32_t data;

  data = pciGetConfigLong(bus, device, function, 0);
  if (deviceIDptr)
    *deviceIDptr = data >> 16;
  return data & 0xffffU;
}

/* Pega o Base Address Register 0 */
__attribute__((regparm(3)))
uint32_t pciGetConfigBAR0(uint8_t bus, 
                          uint8_t device, 
                          uint8_t function, 
                          enum bartype_e *type)
{
  uint32_t data;

  data = pciGetConfigLong(bus, device, function, 0x10);

  /* Repare que o bit 0 do BAR nos indica o
     espaço de endereçamento usado! */
  if ((*type = (enum bartype_e)(data & 1)) == BARTYPE_MEM)
    data &= 0xfffffff0U;  // memory addr.
                          // 16 bytes aligned.
  else	
    data &= 0xfffffffcU;  // I/O addr.
                          // 4 bytes aligned.

  return data;
}

/* Pega a classe do dispositivo. */
__attribute__((regparm(3)))
uint8_t pciGetConfigClass(uint8_t bus, 
                          uint8_t device, 
                          uint8_t function)
{
  return pciGetConfigLong(bus, device, function, 8) >> 24;
}

O arquivo pci.h define, entre outras coisas, o macro PCI_MASTERBUS_ADDR:

/* pci.h */
#ifndef __PCI_INCLUDED__
#define __PCI_INCLUDED__

#include <stdint.h>

/* Número máximo de devices num barramento PCI */
#define PCI_MAXDEVS 64

/* Número máximo de funções num device. */
#define PCI_MAXFUNCS 8

/* Monta o endereço do bus/device/func/reg
   para o configuration space */
#define PCI_CONFIG_ADDR(bus,dev,func,offset) \
  ( ( 1U << 31 ) | \
    ( ((uint32_t)(bus)  & 0xff) << 16 ) | \
    ( ((uint32_t)(dev)  & 0x3f) << 11 ) | \
    ( ((uint32_t)(func) & 0x07) << 8 )  | \
    ( (uint32_t)(offset) & 0xfc) )

__attribute__((regparm(3)))
uint32_t pciGetConfigLong(uint8_t,
                          uint8_t,
                          uint8_t,
                          uint8_t);

/* Retorna VendorID e DeviceID (se deviceIDptr != NULL). */
__attribute__((regparm(3)))
uint16_t pciGetConfigVendorID(uint8_t, 
                              uint8_t, 
                              uint8_t, 
                              uint16_t *);

enum bartype_e {
  BARTYPE_MEM,
  BARTYPE_IO
};

__attribute__((regparm(3)))
uint32_t pciGetConfigBAR0(uint8_t, 
                          uint8_t, 
                          uint8_t, 
                          enum bartype_e *);

__attribute__((regparm(3)))
uint8_t pciGetConfigClass(uint8_t, uint8_t, uint8_t);

#endif

A varredura dos devices no barramento 0 pode ser feita assim:

/* scan.c */
#include <stdio.h>
#include <stdint.h>
#include "pci.h"

/* Essas são as classes padrão da especificação PCI 3 */
const char *classes[] = {
  "Unknown [old]",
  "Mass Storage",
  "Network",
  "Display",
  "Multimedia",
  "Memory",
  "PCI Bridge",
  "Simple Communication",
  "Base System Peripheral",
  "Input Device",
  "Docking Station",
  "Processor",
  "Serial Bus",
  "Wireless",
  "Inteligent I/O",
  "Satellite Communications",
  "Encrypt/Decrypt",
  "DSP",
  [255]="Unknown"
};

void main(void)
{
  uint16_t vendorid, deviceid;
  uint8_t dev, func, class;
  const char *p;
  static const char *reserved = "Reserved";
  static const char *types[] = { "Memory", 
                                 "I/O", 
                                 "<none>" };
  enum bartype_e type;
  uint32_t addr;

  fputs("Dispositivos encontrados (Bus #0):\n", stdout);

  /* Varre todos os dispositivos. */
  for (dev = 0; dev < PCI_MAXDEVS; dev++)
  {
    /* E varre todas as funções de um dispositivo. */
    for (func = 0; func < PCI_MAXFUNCS; func++) { /* Lê VendorID para determinar se o dispositivo existe. */ vendorid = pciGetConfigVendorID(0, dev, func, &deviceid); /* Lê a classe do dispositivo para determinar o que ele é. */ class = pciGetConfigClass(0, dev, func); if (!(p = classes[class])) p = reserved; /* Pega o near pointer (endereço físico) do BAR0 */ addr = pciGetConfigBAR0(0, dev, func, &type); /* Se VendorID for diferente de 0xffff, temos um dispositivo! */ if (vendorid != 0xffff) printf("Device #0x%02hhx:0x%02hhx -> VendorID: %#hx, DeviceID: %#hx,\n"
               "                     Class: %#hhx (%s)\n"
               "                     BAR #0: %#x (%s)\n",
          dev, func, vendorid, deviceid, class, p,
          addr, addr ? types[type] : types[2]);
    }
  }
}

O resultado, no MS-DOS, rodando uma VM no VirtualBox, é este:

Estamos no caminho certo… Note que nem todos os dispositivos fornecem um endereço de acesso (pelo menos, não bo BAR0), mas o Display e Multimedia fornecem endereços base de I/O mapeados em memória e o dispositivo da classe Base System Peripheral fornece um endereço no espaço de I/O (porta).

Para determinar o que são, realmente, os dispositivos, existem outros campos no espaço de configuração que devem ser levados em conta… Além da classe temos a subclasse, que nos diz, por exemplo, se o dispositivo 2:0 é compatível com VGA ou não, ou se o dispositivo 5:0 é uma placa de áudio ou outra coisa “multimídia” qualquer…

O campo header type nos diz, também, a estrutura dos registros que o seguem. O bit 7, por exemplo, indica se estamos lidando com um dispositivo com múltiplas funções…

Se você está lidando com um dispositivo (que não seja uma ponte), o DWORD final, no offset 0x3C da estrutura do configuration space, lhe dará informações sobre interrupções.

Neste ponto, sabendo o endereço base, e algumas características do dispositivo, fica a cargo do próprio dispositivo qualquer tipo de protocolo para seu uso. Por exemplo, mesmo que você possa obter o BAR da sua placa nVidia, sem saber como lidar com ela não vai te ajudar muito… O que quero dizer é que, do ponto de vista do software a especificação PCI só lhe deixará chegar até o ponto onde você obterá configurações do dispositivo.

Aviso sobre o configuration space

Apenas os campos do offset 0 até o offset 15 (os primeiros 4 DWORDs) são comuns a todos os dispositivos PCI. Os demais campos podem ser diferentes. Os dispositivos do PIIX3, por exemplo, costumam deixar disponíveis o BAR em outro lugar que não no offset 16…

Você pode esperar padronização em dispositivos como PCI-to-ISA bridge e PCI-to-PCI bridge, mas todo o resto depende do que obtiver o VendorID, Class, Subclass e Header Type, bem como outros campos…

Configuration space e PCI Express

A especificação PCI Express estende um bocado o configuration space e não exige que o espaço de endereços de I/O (portas 0xCF8 e 0xCFC) sejam usados para acessá-lo. No PCIe, o configuration space é mapeado na memória e, cada função de um device tem um espaço de 1 página (4 KiB) e o acesso é feito de acordo com o endereço base do configuration space adicionado a sequência de bits que monta o barramento, device, function, deslocados 12 bits para a esquerda:

PhysAddr=BaseAddr+((bus\,shl\,20)\,or\,(dev\,shl\,15)\,or\,(func\,shl\,12))

Os 12 bits inferiores são o registrador no configuration space. Resta entender como obter o ponteiro para a região da memória usada como configuration space e isso é feito via ACPI (que está fora do escopo deste artigo).

Essencialmente, PCIe é a mesma coisa que PCI, mas com um método de obtenção das configurações diferente… Felizmente a especificação PCIe nos diz que o método antigo (portas 0xCF8 e 0xCFC) têm que continuar valendo…

MyToyOS: O controlador de teclado (e mouse)

Já falei sobre o PIC, o PIT e o DMAC aqui. Eis o controlador “de teclado”, KBDC (KeyBoarD Controller), cujo nome engana um bocado. O chip 8042 (legado) é, na verdade, um microcontrolador que provê um protocolo específico para troca de dados entre o processador e dispositivos “seriais”. Ele lida, no PC, principalmente com o teclado e com o mouse. Inesperadamente, os dados de ambos os dispositivos trafegam via as mesmas portas de I/O. Ou seja, do ponto de vista do software, teclado e mouse são, ambos, teclados com padrões de stream de dados diferentes. Veremos como isso funciona adiante…

As portas para acesso ao KBDC primário (podem existir 2) são 0x64 e 0x60, onde a porta 0x64 é alocada para obtenção de status (se lida) ou para enviar comandos (se escrita) para o 8042. A porta 0x60 é a porta de dados que deve ser lida ou escrita somente de acordo com o estado de alguns flags contidos na porta de status indicarem essa possibilidade. A porta de status tem o seguinte bitmap:

Bit Mnemônico Descrição
0 OBF Output Buffer Full
1 IBF Input Buffer Full
2 SYS (usada internamente)
3 A2 (usada internamente)
4 INH “Teclado” habilitado
5 TxTO Transmmit TIMEOUT
6 RxTO Receive TIMEOUT
7 PERR Erro de Paridade

Estamos interessados nos bits OBF e IBF apenas. Eles indicam se os “buffers” (registradores) de entrada ou saída contém ou não dados… Lembre-se que o KBDC lida com dispositivos seriais, então, ao escrevermos um comando para o KBDC, este tem que ser repassado para o dispositivo de forma serializada. Isso pode levar um tempo, no qual o “buffer” de “entrada” estará cheio e não pode receber novos comandos. Isso é indicado no bit IBF. A mesma coisa acontece quando o KBDC recebe um byte, quando ele for completamente recebido e colocado no buffer de “saída”, isso é indicado no bit OBF.

Note que “entrada” e “saída”, aqui, são consideradas do ponto de vista do controlador. Ao receber a notificação do teclado que existe uma tecla para ser lida, o KBDC receberá esse scan code e o colocará na “saída” (output) para a futura leitura do processador… Ao receber um comando vindo do processador, este é direcionado para o dispositivo, via KBDC, através de seu registrador (buffer) de “entrada”.

O termo “buffer” aqu nada tem a ver com o “buffer do teclado”, que suporta o enfileiramento de até 16 teclas…

O código abaixo testa OBF antes de tentar ler um scan code vindo do teclado…

wait_kbdc_ob_full:
  in   al,0x64
  test al,1              ; Testa OBF.
  jz   wait_kbdc_ob_full ; OBF=0? fica no loop.

  ; Aqui OBF será 1. Estamos prontos para ler um
  ; dado da porta 0x60.
  ret

Da mesma forma, para escrevermos um comando na porta 0x64 ou um dado na porta 0x60, temos que ter certeza que o “input” buffer esteja vazio verificando se IBF=0!

Teclado ou mouse?

O KBDC não é dedicado ao teclado. Tanto o mouse quanto o teclado são lidos no mesmo par de portas (0x60 e 0x64)…

O KBDC pode controlar até 2 dispositivos seriais e, também, gerar IRQs para anunciar a disponibilidade de dados deles… O método que mostrei anteriormente, fazendo polling do bit OBF, não é o ideal… Nos PCs, o primeiro dispositivo (teclado) está associado à IRQ1, e o segundo (mouse), à IRQ12… No caso do mouse, geralmente a IRQ12 é implementada pelo driver (no caso do MS-DOS, MOUSE.SYS) ou pelo sistema operacional e, simplesmente, lê o stream de dados que é composto das coordenadas X e Y e o estado dos 3 botões (pressionados ou não):

bit: 7 6 5 4 3 2 1 0
byte 1: Y overflow X overflow Y sign X sign 1 middle button right button left button
byte 2: X
byte 3: Y

X e Y são sinalizados. Ou seja, o movimento pode ser feito num sentido ou em outro, nos eixos correspondentes, mas note que o valor informado é o delta em relação à última leitura. Os bits de overflow estão ai para quando você desloca o mouse muito rápido (tipo, ficou com raiva de um bug no seu código e jogou o mouse na parede!).

O fato é que, se o mouse está associado à IRQ12, essa IRQ lerá os 3 bytes, limpando o buffer de saída do KBDC. Assim, ao ler a porta 0x60, fora da IRQ12, estaremos lidando garantidamente com o teclado, não com o mouse. Por default, o mouse está desabilitado e a BIOS tende a não configurá-lo.

Hoje em dia os mouses têm uma “roda” (mouse wheel) que é informada num 4º byte contendo a coordenada Z. Na inicialização do mouse deve-se determinar se ele informa essa 3ª coordenada… Isso é feito por algum comando enviado ao KBDC (veja, mais adiante, a explicação).

Os dados vindos do teclado

Outra vez, diferente da intuição, os dados vindos do teclado não correspondem ao código ASCII das teclas pressionadas, mas a um código da própria tecla… Afinal de contas, o “significado” da tecla nada mais é do que o silk screen que foi pintado em cima dela…

O teclado envia ao KBDC o que chamamos de scan codes. Num teclado ENG-US os scan codes são como mostrados no gráfico abaixo:

E, para as demais teclas:

Isso quer dizer que, se recebermos 0x70 na porta 0x60, então o usuário pressionou a tecla ‘0’ do teclado numérico… Se recebermos 0x45, ele pressionou a tecla ‘0’ do teclado “alfanumérico”… O bit 7 desse código nos diz se a tecla foi pressionada ou solta… Se recebermos 0xF0, o ‘0’ do teclado numérico foi liberado.

Repare que não existe um código 0xE0 ou 0xE1 isolados. Assim, se recebermos 0xE0 ou 0xE1, existirão mais 1 ou 2 bytes no scan code (os 4 bits inferiores de 0xE?) nos dizem quantos, basta adicionar 1)… As setas, por exemplo, têm scan codes de 2 bytes, começados por 0xE0… Note que o byte seguinte é o mesmo que gosta no teclado numérico (dê uma olhada no silk screen do seu teclado)… O mesmo aconte com PgUp, PgDn, Ins, Del, Home e End.

Outro detalhe importante… para demonstrar que scan codes nada têm a ver com os códigos ASCII, basta notar que o teclado não diferencia entre um ‘?’ e um ‘/’ ou um ‘A’ e um ‘a’. A tecla ‘A’ tem scan code 0x1C para ambos os casos. Quem faz a diferenciação é a BIOS ou o sistema operacional… A mesma coisa acontece com as teclas “especiais” como Alt, Shift e Ctrl (e a tecla “Super” — aquela com o logo do Windows — cujo scan code provavelmente é 0xE0,0x5B para o LWin e 0xE0,0x5C para o RWin). Note, também, que teclas adicionais terão scan codes próprios e, provavelmente, precedidos de 0xE0 ou 0xE1.

Existem 3 conjuntos de scan codes diferentes!

Dependendo de como o teclado for configurado, os scan codes obtidos podem não ser os que mostrei acima… Esse ai é o conjunto 1. Teclados suportam os conjuntos 2 e 3. Para selecionar o conjunto basta enviar o comando 0xF0 e escrever 0x01, 0x02 ou 0x03 na porta de dados. Se escrever 0x00, podemos ler o conjunto atualmente selecionado (provavelmente 1).

Além do teclado e mouse

Como ficou claro, podemos enviar comandos para o KBDC para que ele faça coisas além de ler scan codes ou posições do mouse. Basta escrever em 0x64 o comando desejado (desde que o IBF esteja vazio) e obedecer a semântica do comando para ler/escrever na porta 0x60, de acordo com o necessário. Por exemplo, podemos acender ou apagar os LEDs de caps lock, scroll lock e num lock via comando 0xED, bastando enviar um valor de 3 bits (scroll lock no bit 0, num lock no bit 1 e caps lock no bit 2; os demais bits zerados) para a porta 0x60. Devemos depois esperar por um byte na mesma porta 0x60 que deve ser 0xFA (ACK) ou 0xFE (Resend).

Existem outros comandos que não exigem esse tipo de handshake… Por exemplo, o KBDC pode ser usado para habilitar o Gate A20, assim:

kbdc_enable_a20:
  call  wait_kbdc_ib_empty
  mov   al,0xad       ; Desabilita teclado.
  out   0x64,al

  call  wait_kbdc_ib_empty
  mov   al,0xd0       ; Lê "output port A".
  out   0x64,al

  call  wait_kbdc_ob_full
  in    al,0x60       ; Pega o dado da "output port A".
  mov   cl,al         ; Guarda.

  call  wait_kbdc_ib_empty
  mov   al,0xd1       ; Comando: escreve próximo na
  out   0x64,al       ;    byte na "output port A".

  call  wait_kbdc_ib_empty
  mov   al,cl         ; Recupera "output port A" lida
  or    al,2          ; antes e seta o bit "Gate A20".
  out   0x60,al

  call  wait_kbdc_ib_empty
  mov   al,0xae       ; Habilita teclado.
  out   0x64,al

  ; Espera até que o comando seja terminado...
  call  wait_kbdc_ib_empty
  ret

Existem mais comandos disponíveis… Obviamente alguns são para habilitar IRQs e configurar teclado e mouse… Uma referência dos comandos pode ser encontrada aqui. Neste mesmo artigo do OSDev você poderá observar como fazer a “detecção” do dispositivo ligado ao KBDC, como o mouse com mouse wheel ou até mouses com 5 botões!