Trapaceando… um pouquinho…

Alguns me perguntam sobre minha proficiência na linguagem assembly. Ela é extensa, mas isso não significa que eu não trapaceie de tempos em tempos… Por exemplo: Recentemente tive que criar uma rotina que calculava o endereço LBA de um setor, em disco, com base na estrutura CHS, usada pelos serviços básicos da “int 0x13”. A rotina de leitura de um ou mais setores do disco, pelo serviço 2 requer os seguintes parâmetros:

AH = 2
AL = nº de setores a serem lidos
CH = bits 7~0 do nº do cilindro
CL = bits 7~6 (bits 9~8 do nº do cilindro)
     bits 5~0 (bits 5-0 do nº do setor)
DH = nº da cabeça
DL = nº do drive
ES:BX = endereço lógico do buffer.

Um dos problemas é a codificação do cilindro e setor. Essa mesma codificação consta na tabela de partição de um HD e, portanto, é útil que eu tenha uma rotina que converta esse formato, junto com o nº da cabeça, para um endereço linear (LBA). A rotina, que toma em AX a codificação cilindro/setor e em DL o número da cabeça poderia ser assim:

; chs2lba
; Entrada: AX = cilindro/setor (como na int 0x13).
;          DL = cabeça (byte inferior).
; Saída:   EAX = lba.
; Destrói: EBX, ECX, EDX, ESI e EDI.

chs2lba:
  movzx ecx,byte [num_heads_per_cylinder]
  movzx edi,byte [num_sectors_per_track]

  movzx edx,dl          ; Zera bits superiores de EDX.

  mov   esi,eax
  shr   si,6            ; ESI contém apenas o cilindro
                        ; invertido.
  mov   ebx,eax

  and   esi,3           ; Isola os 2 bits superiores
                        ; do cilindro em ESI.

  and   ebx,0x3f        ; Isola o setor em EBX.

  sal   esi,8           ; Coloca os 2 bits superiores
                        ; do cilindro no lugar certo.

  imul  edi,ecx         ; EDI = hpc * spt.

  movzx ecx,ah          ; ECX = 8 bits inferiores
                        ; do cilindro.
  lea   eax,[esi+ecx]   ; EAX = cilindro.

  imul  eax,edi         ; EAX = c * (hpc * spt).
  imul  eax,edx         ; EAX = (c * hpc * spt)*h
  lea   eax,[ebx+eax-1] ; EAX = (c * hpc * h * spt)+s-1.

  ret

Embora o código claramente use instruções do 386, ele funciona tanto no modo real quanto no protegido… mas, se você parar para analisá-lo, verá que é um cadinho complicado de entender. Mas, como é que eu cheguei a ele?! Ora bolas, eu trapaceei! O código é, na verdade, esse aqui:

/* crt.c
   Compile com:
     gcc -m32 -O2 -S -masm=intel -ffreestanding crt.c 
*/
typedef unsigned char  u8;
typedef unsigned short u16;
typedef unsigned int   u32;

/* Codificação usada pela BIOS */
struct cyl_sec_t {
  u16 sec:6;
  u16 cyl_hi:2;
  u8  cyl_lo;
};

/* Valores obtidos via BIOS. */
extern u8 num_sectors_per_track;
extern u8 num_heads_per_cylinder;

__attribute__((regparm(2)))
u32 chs2lba(struct cyl_sec_t cs, u8 h)
{
  u32 cyls = cs.cyl_lo + ((u32)cs.cyl_hi << 8);
  return (cyls * num_heads_per_cylinder * h) * 
           num_sectors_per_track + (cs.sec - 1);
}

A equação da função chs2lba() pode ser obtida em sites como OSDev ou, até mesmo, Wikipedia (aqui):

\displaystyle lba=(c*hpc*h)*spt+(s-1)

Onde hpc é o nosso num_heads_per_cylinder e spt é o num_sectors_per_track, que obteremos via BIOS. Os valores c, h e s são o cilindro, cabeça e setor, respectivamente.

Para contextualizar, cilindros, cabeças e setores são as 3 dimensões necessárias para localizar um bloco de 512 bytes na “geometria” do disco:

Cilindros, setores e trilhas.
Cilindros, setores e trilhas.

A figura acima não mostra as “cabeças”, mas elas são fáceis de entender… cada “prato” tem dois lados, então a cabeça 0 lê/escreve do lado de cima do primeiro prato. A cabeça 1, a parte de baixo dele e assim por diante…

Em C a rotina é bastante direta: Convertemos os dados da estrutura para obtermos o número do cilindro e aplicamos a equação. O código gerado não é tão evidente assim e o compilador assume algumas coisas:

  1. Ele usará a convenção de chamada apropriada. Registradores como EBX, ESI, EDI e EBP serão salvos na pilha e recuperados depois;
  2. Caso a rotina contenha loops, ele assumirá que os pontos de entrada deverão ser alinhados.

Você pode não querer nem um e nem outro em um código em assembly puro, cuja finalidade seja ter o menor código possível. Por isso eu retirei as salvaguardas de EBX, EDI e ESI do código gerado e acrescentei, na descrição, que esses são destruídos. E já que o GCC permite a destruição de ECX, incluí-o na lista também.

O uso da opção -ffreestanding serve para que o compilador não use nenhuma função intrínseca, ou seja, nada printf, por exemplo.

Felizmente, para mim, o código gerado pelo GCC não admite grandes otimizações… Na verdade, dependendo da arquitetura a ordem em que as instruções se encontram pode ser alterada para ganharmos um ou dois ciclos de máquina extras. Isso pode também ser conseguido com o GCC… O código inicial foi criado com a linha de comando “gcc -O2 -S -m32 -ffreestanding -masm=intel crt.c“. Isso criará código genérico (para o 386, provavelmente), mas o Pentium tem necessidades especiais, por exemplo, é conveniente emparelhar instruções para obter melhor performance. Embora possamos conseguir isso adicionando a opção “-march“, como em “gcc -O2 -S -m32 -march=pentium4 -ffreestanding -masm=intel crt.c“, o compilador não nos ajuda muito… Otimizações para processadores específicos devem ser feitas manualmente se você quer arrancar a menor quantidade de ciclos de uma rotina.

Vantagens e desvantagens da trapaça:

Em primeiro lugar, o GCC tratará o tamanho de cada variável da forma correta, desde que você obedeça a especificação da linguagem C. Isso significa que multiplicações e divisões podem nunca incorrer em overflows porque o compilador escolhe trabalhar com uma precisão maior que a necessária… Isso pode gerar um problema também! Ao multiplicar dois valores não sinalizados de 64 bits (unsigned long long, por exemplo), no modo i386, o compilador poderá escolher chamar uma função especializada para a qual ele não fornecerá a listagem em assembly…

Outra vantagem é que o compilador é esperto. Ele tentará encontrar soluções que você provavelmente não pensou. Experimente escrever, em assembly, o código em C acima e verá que é possível que seu código seja bem menos performático e bem maior!

Mas, nem tudo são flores… Algumas vezes o compilador gera código esquisito que, claramente, poderia ser melhor. É raro, mas acontece… Como sempre, eu recomendo que os desenvolvedores revisem seus códigos feitos em C, em assembly!! Muitas vezes você encontrará um caminho melhor, em C, que gerará código melhor, em assembly, e poderá usar a técnica descoberta para casos similares.

O código gerado é para o modo protegido!

Embora eu tenha falado que esse meu código funcione tanto no modo real quanto no protegido, isso é uma constatação feita ao analisar o código gerado, em assembly. O compilador cria código pronto para ser usado em modo protegido! Ele assume que os seletores de segmento são pré inicializados (e jamais os altera) e, dependendo da opção de compilação, que os ponteiros têm 32 ou 64 bits de tamanho, jamais 16. Assim, uma instrução do tipo mov eax,[esi] poderá não funcionar no modo real, da forma como você imagina, dependendo do valor de ESI. Pode ser necessário adaptar os ponteiros, do modo protegido para o real…

A opção -m16, disponível a partir do GCC 4.9, não é de grande ajuda… Essa opção apenas acrescenta uma diretiva: .code16gcc na listagem assembly (e no objeto para o linker), que transforma o código 386 para o modo real (acrescenta os prefixos 0x66 e 0x67, quando necessários)… Isso torna sua rotina umas duas vezes maior do que ela precisaria ser… E os endereçamentos continuam sendo feitos em 32 bits (exigindo o prefixo 0x67), mesmo que eles sejam “ceifados” para 16.

Ficam os avisos: Cuidado ao trapacear!

Anúncios