Sempre dá para melhorar um bocado…

Eis duas formas de escrever uma função. A segunda forma fica meio “ofuscada”, mas, como veremos, o tamanho e performance valem a pena… A função é usada no T50, que deixei no primeiro modelo para ficar mais legível:

static const char *suffixes[] = { "st", "nd", "rd", "th" };

const char *get_ordinal_suffix(unsigned int n)
{
  /* 11, 12 & 13 have 'th' suffix, not 'st, nd or rd'. */
  if ((n < 11) || (n > 13))
    switch (n % 10)
    {
    case 1:
      return suffixes[0];
    case 2:
      return suffixes[1];
    case 3:
      return suffixes[2];
    }

  return suffixes[3];
}

Bem simples e aparentemente direto… O código gerado é esse:

bits 64

section .rodata

suffix1:   db "st",0
suffix2:   db "nd",0
suffix3:   db "rd",0
suffix4:   db "th",0

section .text

global get_oridinal_suffix
get_ordinal_suffix:
  lea  edx,[rdi-11]
  mov  eax,suffix4
  cmp  edx,2
  jbe  .L1

  ; Isso é n = n % 10!
  mov  eax,edi
  mov  edx,3435973837
  mul  edx
  shr  edx,3
  lea  eax,[rdx + rdx*4]
  add  eax,eax
  sub  edi,eax

  mov  eax,suffix2
  cmp  edi,2
  je   .L1
  mov  eax,suffix3
  cmp  edi,3
  je   .L1
  cmp  edi,1
  mov  edx,suffix4
  mov  eax,suffix1
  cmovne rax,rdx

.L1:
  ret

Não fique espantado com a mágica que o compilador faz para calcular o resto inteiro da divisão por 10. Ele prefere usar uma multiplicação do que divisão por questão de performance (multiplicações são lentas, mas divisões são verdadeiras lesmas!).

Na rotina acima não há muito como escapar do cálculo do resto da divisão por 10 por causa das duas exceções (11 e 12), mas podemos melhorar um cadinho o switch usando um macete:

const char *get_ordinal_suffix2(unsigned int n)
{
  if (n >= 11 && n <= 13) return suffixes[3];

  return suffixes["\003\000\001\002\003"
                  "\003\003\003\003\003"[n % 10]];
}

O uso de uma string literal como endereço base para a própria string é perfeitamente válido em C já que a string literal nada mais é do que a declaração dos seus bytes e o retorno é o endereço base. Note que cada “caracter” do array foi codificado como sendo o índice de outro array (suffixes) e, por isso, a rotina final será assim:

...
section .rodata

_lstr: db 3,0,1,2,3,3,3,3,3,3,3,3,3,3,3,3,0
suffixes: dq suffix1, suffix2, suffix3, suffix4

section .text
...
  global get_ordinal_suffix2
get_ordinal_suffix2:
  lea  edx,[rdi-11]
  mov  eax,suffix4
  cmp  edx,2
  jbe  .L1

  ; Isso ainda é n = n % 10!
  mov  eax,edi
  mov  edx,3435973837
  mul  edx
  shr  edx,3
  lea  eax,[rdx + rdx*4]
  add  eax,eax
  sub  edi,eax

  movsx eax,byte [_lstr + rdi]
  mov  rax,[suffixes + rax*8]
.L1:
  ret  

As 10 instruções do switch foram substituídas por apenas duas!

Aliás, quanto ao “macete” da string, acima, podemos fazer uso de coisas como essas sem problemas:

char c1 = "frederico"[5]; // c1 = 'r'.
char c2 = 5["frederico"]; // c2 = 'r';
char k = (n % 10)["9876543210"]; // desde que n seja unsigned.

Todas essas construções são perfeitamente válidas…

O macete do uso de estruturas para acessar argumentos de funções…

Alguns de meus leitores acharam estranho a maneira com que acesso os argumentos de funções passadas pela pilha, como é o caso de convenções de chamadas usadas no modo i386, na maioria dos sistemas operacionais. Algumas pessoas estão acostumadas a usar um prólogo e um epílogo nas suas rotinas e usam o registrador EBP como ponteiro base, assim:

MyFunc:
  push ebp     ; Prólogo
  mov  ebp,esp

  mov  eax,[ebp+8] ; Pega o 1º argumento.
  ...

  pop  ebp     ; Epílogo
  ret

Ok… esse é o método padrão, mas apresenta um problema: Se você errar o offset em relação a EBP vai pegar o valor errado! E se pudéssemos explicitar a posição, na pilha, usando símbolos? Well… podemos! Usando estruturas. O mesmo fragmento de código, acima, poderia ser escrito assim:

struc MyFuncStack
  .oldebp:   resd 1 ; ESP aponta para o EBP empilhado.
  .retaddr:  resd 1 ; CALL empilha o endereço de retorno.
  .arg1:     resd 1 ; Primeiro argumento.
endstruc

MyFunc:
  push ebp     ; Prólogo
  mov  ebp,esp

  mov  eax,[ebp+MyFuncStack.arg1] ; Pega o 1º argumento.
  ...

  pop  ebp     ; Epílogo
  ret

A expressão MyFuncStack.arg1, no NASM, será traduzida como o offset de arg1 em relação ao início da estrutura, ou seja, 8. Isso evitará erros e, uma vez que você se acostume com esse “macete”, torna seu código mais legível.

Aliás, usar um prólogo e um epílogo é supérfluo. Podemos escrever o mesmo fragmento de rotina assim:

struc MyFuncStack
  .retaddr:  resd 1
  .arg1:     resd 1
endstruc

MyFunc:
  mov  eax,[esp+MyFuncStack.arg1] ; Pega o 1º argumento.
  ...
  ret

Mas… e se eu quiser usar variáveis locais alocadas na pilha, como faço? Basta alterar a estrutura e modificar ESP de acordo:

struc MyFuncStack
  .local1:   resd 1 ; Nossa variável local
  .localsize:
  .retaddr:  resd 1
  .arg1:     resd 1
endstruc

MyFunc:
  add  esp,MyFuncStack.localsize
  mov  eax,[esp+MyFuncStack.arg1] ; Pega o 1º argumento.
  ...
  sub  esp,MyFuncStack.localsize
  ret

Note que a estrutura do seu stack frame precisa conter o estado em que você deixou a pilha… É o exemplo do uso do epílogo e do prólogo: Coloquei, lá em cima, um oldebp no frame… se eu tivesse salvo algo mais na pilha, teria que colocar no frame também, por exemplo:

struc MyFuncStack
  .local1:   resd 1 ; Nossa variável local
  .localsize:
  .oldebx:   resd 1
  .oldebp:   resd 1
  .retaddr:  resd 1
  .arg1:     resd 1
endstruc

MyFunc:
  push ebp
  push ebx
  add  esp,MyFuncStack.localsize
  mov  eax,[esp+MyFuncStack.arg1] ; Pega o 1º argumento.
  ...
  sub  esp,MyFuncStack.localsize
  pop  ebx
  pop  ebp
  ret

Assim eu posso usar EBP para outra coisa que não a base da pilha, por exemplo…

A futilidade de programar para Windows diretamente em assembly…

Deixe-me mostrar como o código em C do “hello, world” (mais simples, sem menus ou ícone customizado) é codificado em assembly para ambas as plataformas i386 e x86-64. Vou me deter na função WinMain() aqui, sem mostrar o tratador de mensagens (que é bem mais simples). O código que mostrarei é esse:

#include <windows.h>

LRESULT CALLBACK WindowMessagesHandler(HWND, UINT,
                                       WPARAM, LPARAM);

int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst,
                     LPSTR lpszCmdLine, int nCmdShow)
{
  HWND hWnd;
  MSG msg;
  static const char *className = "MyWinAppClass32";
  WNDCLASS wc = {
    .style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
    .lpfnWndProc = WindowMessagesHandler,
    .hInstance = hInstance,
    .hIcon = LoadIcon(NULL, IDI_APPLICATION),
    .hCursor = LoadCursor(NULL, IDC_ARROW),
    .hbrBackground = (HBRUSH)(COLOR_WINDOW+1),
    .lpszClassName = className
  };

  RegisterClass(&wc);

  if ((hWnd = CreateWindow(className,
                           "My Win32 App",
                           WS_OVERLAPPEDWINDOW,
                           CW_USEDEFAULT, CW_USEDEFAULT,
                           640, 480,
                           NULL,
                           NULL,
                           hInstance,
                           NULL)) == NULL)
  {
    MessageBox(NULL, "Error creating window!", "Error",
               MB_OK | MB_ICONERROR);
    return 0;
  }

  UpdateWindow(hWnd);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&msg, NULL, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  return msg.wParam;
}

Em primeiro lugar, será interessante notar que todas as chamadas de funções da Win32 API são feitas de forma indireta. Não tem outro jeito, já que essas funções estão localizadas em DLLs: kernel32.dll, user32.dll e msvcrt.dll. O outro detalhe é a diferença entre o nome das funções nas arquiteturas i386 e x86-64: Na primeira o nome é sempre precedido de “__imp__” e sucedido do tamanho (em bytes) da lista de parâmetros que a função recebe. Por exemplo, A função RegisterClass() é importada como __imp__RegisterClass@4. No caso da plataforma x86-64 o prefixo “__imp__” continua lá e o acesso indireto também, mas esse sufixo não existe.

Eis o código aproximado, parcial, de WinMain() para i386 (NASM):

bits 32
; win32.inc não existe. temos que fazê-lo menualmente.
%include "win32.inc"

section .data

className: db "MyWinAppClass32",0
wintitle:  db "My Win32 App",0

wc: istruc WNDCLASS
    iend

msg: istruc MSG
     iend

section .bss

hWnd: resd 1

section .text

struc WinMainStack
  .oldebp:      resd 1
  .retaddr:     resd 1
  .hInstance:   resd 1
  .hPrevInst:   resd 1
  .lpszCmdLine: resd 1
  .nCmdShow:    resd 1
endstruc

; Definida em outro lugar
extern _WindowMessagesHandler@16

global _WinMain@16
_WinMain@16:
  push ebp
  mov  ebp,esp

  ; Preenche a estrutura em wc.
  xor eax,eax
  mov ecx,WNDCLASS.size
  lea edi,[wc]
  cld
  rep stosb
  mov  eax,[ebp+WinMainStack.hInstance]
  mov  [wc+WNDCLASS.hInstance],eax
  mov  dword [wc+WNDCLASS.style],(CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS)
  mov  dword [wc+WNDCLASS.lpfnWndProc],_WindowMessagesHandler@16
  push dword IDI_APPLICATION
  push dword 0
  call dword [__imp__LoadIconA@8]
  mov  [wc+WNDCLASS.hIcon],eax
  push dword IDC_ARROW
  push dword 0
  call dword [__imp__LoadCursorA@8]
  mov  [wc+WNDCLASS.hCursor],eax
  mov  dword [wc+WNDCLASS.hbrBackground],(COLOR_WINDOW+1)
  mov  dword [wc+WNDCLASS.lpszClassName],className

  ; Registra a classe
  push wc
  call dword [__imp__RegisterClassA@4]

  ; Cria a janela.
  xor eax,eax
  push eax
  push dword [ebp+WinMainStack.hInstance]
  push eax
  push eax
  push dword 240
  push dword 320
  push dword CW_USEDEFAULT
  push dword CW_USEDEFAULT
  push dword WS_OVERLAPPEDWINDOW
  push wintitle
  push className
  push eax
  call dword [__imp__CreateWindowExA@48]

  ; Testa para ver se conseguiu...
  test eax,eax
  jnz  ok

  ; Não conseguiu...
  push dword (MB_OK | MB_ICONERROR)
  push caption_msg
  push title_msg
  push dword 0
  call dword [__imp__MessageBoxA@16]
  xor  eax,eax
  jmp  WinMainExit

  ; Conseguiu criar a janela...
ok:
  mov  [hWnd],eax

  ; Atualiza e mostra a janela.
  push eax
  call dword [__imp__UpdateWindow@4]
  push dword [ebp+WinMainStack.nCmdShow]
  push dword [hWnd]
  call dword [__imp__ShowWindow@8]

MessageLoop:
  xor eax,eax
  push eax
  push eax
  push eax
  push msg
  call dword [__imp__GetMessageA@16]
  test eax,eax
  jz   MessageLoopExit
  push msg
  call dword [__imp__TranslateMessage@4]
  push msg
  call dword [__imp__DispatchMessage@4]
  jmp  MessageLoop

MessageLoopExit:
  ; Saiu do loop, retorna msg.wParam.
  mov  eax,[msg+MSG.wParam]

WinMainExit:
  pop  ebp
  ret  16      ; Livra-se dos 16 bytes da pilha!

Que diabos é esse ‘A’ na frente do nome de algumas funções da API? Por exemplo: __imp__CreateWindowExA@48… Acontece que Windows faz distinção entre funções que aceitam strings no formato ANSI (ou seja, Windows-1252 — uma variação do charset ISO-8859-1) e UNICODE (Windows usa UTF-16, não UTF-8!) conhecida como WideChar. Assim, em C, a função CreateWindowEx é um apelido para CreateWindowExA (que espera strings ANSI), cujas strings serão convertidas, internamente, para UNICODE… Se quiser trabalhar diretamente com UNICODE você deve usar CreateWindowExW. Sempre que uma função tomar ponteiros para strings, direta ou indiretamente, ela deve ser seguida de A ou W, de acordo com o charset que você estiver usando.

Você deve estar achando estranho o fato do SDK da Microsoft usar, por padrão, apelidos para as varições ANSI das funções já que, internamente, ele sempre usa UNICODE… Essa é mais uma característica histórica… Antes da Win32 API ninguém usava UNICODE (nem existia!). Quando o Windows NT 3 surgiu, as versões WideChar das funções foram incorporadas, mas a Microsoft quis manter compatibilidade com o Win16. Daí os apelidos…

Além do ‘A’ e do ‘W’, note também que usei a função CreateWindow, no código em C, sem o Ex no final… Essa função é obsoleta, mas o apelido consta em winuser.h como CreateWindowExA, onde o primeiro argumento dessa nova função é uma constante para estilos extras de janela…

A listagem em assembly acima serve a dois propósitos: Mostra que codificar um programa para Windows diretamente em assembly é tarefa fútil, uma vez que o compilador C faz um trabalho melhor… Por exemplo: Diferente de meu código, o gcc mantém os ponteiros para as funções contidos em __imp__GetMessageA@16, __imp__TranslateMessage@4 e __imp__DispatchMessage@4 nos registradores ESI, EBP e EDI, respectivamente, daí ele realiza as chamadas via registradores, no loop de mensagens:

  lea esi,[__imp__GetMessageA@16]
  lea ebp,[__imp__TranslateMessage@4]
  lea edi,[__imp__DispatchMessage@4]
MessageLoop:
  xor  eax,eax
  push eax
  push eax
  push eax
  push msg
  call esi
  test eax,eax
  jz   MessageLoopExit
  push msg
  call ebp
  push msg
  call edi
  jmp  MessageLoop
MessageLoopExit:

Compare o código que mostrei lá em cima com o gerado via:

$ i586-mingw32msvc-gcc -O3 -S -masm=intel winapp.c

O outro propósito da listagem é mostrar que, no modo i386, Windows usa um padrão parecido com PASCAL para passagem de parâmetros, necessariamente usando a pilha e empilhando da direita para a esquerda… Isso difere muito da arquiteura x86-64 onde, por padrão, registradores são usados para passar parâmetros. Compare o código do WinMain, acima, com o gerado pelo x86_64-w64-mingw32-gcc (código aproximado, em NASM):

bits 64
; win32-64.inc não existe. temos que fazê-lo menualmente.
%include "win32-64.inc"

section .data

className: db "MyWinAppClass32",0
wintitle:  db "My Win32 App",0

wc: istruc WNDCLASS
    iend

msg: istruc MSG
     iend

section .bss

hWnd: resq 1

section .text

struc WinMainStack
  .oldrbp:      resq 1
  .retaddr:     resq 1
  .hInstance:   resq 1
  .hPrevInst:   resq 1
  .lpszCmdLine: resq 1
  .nCmdShow:    resd 1
endstruc

; Definida em outro lugar
extern WindowMessagesHandler

global WinMain
WinMain:
  push rbp
  mov  rbp,rsp

  ; Preenche a estrutura em wc.
  xor  eax,eax
  mov  ecx,WNDCLASS_size
  lea  rdi,[wc]
  cld
  rep  stosb
  mov  rax,[rbp+WinMainStack.hInstance]
  mov  [wc+WNDCLASS.hInstance],rax
  mov  dword [wc+WNDCLASS.style],(CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS)
  mov  qword [wc+WNDCLASS.lpfnWndProc],WindowMessagesHandler

  ; ecx já contém 0.
  mov  edx,IDI_APPLICATION
  call qword [__imp__LoadIconA]
  mov  [wc+WNDCLASS.hIcon],rax

  xor  ecx,ecx
  mov  edx,IDC_ARROW
  call qword [__imp__LoadCursorA]
  mov  [wc+WNDCLASS.hCursor],rax

  mov  dword [wc+WNDCLASS.hbrBackground],(COLOR_WINDOW+1)
  mov  qword [wc+WNDCLASS.lpszClassName],className

  ; Registra a classe
  lea rcx,[wc]
  call qword [__imp__RegisterClassA]

  ; Cria a janela.
  xor ecx,ecx    ; exStyle = 0
  lea rdx,[className]
  lea r8,[wintitle]
  mov r9d,WS_OVERLAPPEDWINDOW
  push rcx
  push qword [rbp+WinMainStack.hInstance]
  push rcx
  push rcx
  push dword 240
  push dword 320
  push dword CW_USEDEFAULT
  push dword CW_USEDEFAULT
  call qword [__imp__CreateWindowExA]

  ; Testa para ver se conseguiu...
  test rax,rax
  jnz  ok

  ; Não conseguiu...
  xor  ecx,ecx
  lea  rdx,[title_msg]
  lea  r8,[caption_msg]
  mov  r9d,(MB_OK | MB_ICONERROR)
  call qword [__imp__MessageBoxA]
  xor  eax,eax
  jmp  WinMainExit

  ; Conseguiu criar a janela...
ok:
  mov  [hWnd],rax

  ; Atualiza e mostra a janela.
  mov  rcx,rax
  call qword [__imp__UpdateWindow]
  mov  rcx,[hWnd]
  mov  edx,[rbp+WinMainStack.nCmdShow]
  call qword [__imp__ShowWindow]

MessageLoop:
  lea  rcx,[msg]
  xor  edx,edx
  mov  r8d,edx
  mov  r9d,edx
  call qword [__imp__GetMessageA]
  test rax,rax
  jz   MessageLoopExit
  lea  rcx,[msg]
  call qword [__imp__TranslateMessage]
  lea  rcx,[msg]
  call qword [__imp__DispatchMessage]
  jmp  MessageLoop

MessageLoopExit:
  ; Saiu do loop, retorna msg.wParam.
  mov  eax,[msg+MSG.wParam]

WinMainExit:
  pop  rbp
  ret

A convenção de chamada x86-64, usada pelo Windows, usa os registradores RCX, RDX, R8 e R9 para os 4 primeiros parâmetros e os demais são empilhados no mesmo padrão PASCAL. A outra diferença está na nomenclatura das funções importadas. Os nomes dos símbolos são mais “simples” sem aquele “@n” no final. E, por fim, você não precisa mais prestar atenção para a quantidade de bytes usados nos argumentos (repare no RET final).

Mesmo com essas facilidades o GCC realiza tarefa melhor que o código acima.

Tenho que, agora, responder uma pergunta simples: Como é que o compilador sabe o ponteiro das funções contidas em DLLs?

Que bruxaria é essa?

Aparentemente os ponteiros, todos prefixados com __imp__ são conhecidos de antemão pela nossa aplicação e referem-se às funções da Win32 API. Nosso código não os inicializou em nenhum lugar! Como isso é possível?

DLLs são carregadas de duas maneiras: Ou na inicialização da aplicação (early binding que em tradução livre significa “ligação antecipada”) ou em tempo de execução (late binding, “ligação tardia”). No primeiro caso, ao linkar o nosso arquivo objeto, usamos bibliotecas estáticas contendo códigos de inicialização, inclusive da lista de DLLs informadas no linker. No segundo caso (late binding), temos que usar a função LoadLibrary() da API — que carregará a DLL no espaço de endereçamento da nossa aplicação — e GetProcAddress(), para obter o ponteiro para a função. É claro que essas funções também estão em DLLs, mas elas são, obrigatoriamente, early boundedkernel32.dll e user32.dll. Assim, os ponteiros para algumas funções da API estarão sempre disponíveis para nossa aplicação.

No caso do uso de DLLs early bounded o GCC tem um atalho: Ele permite a linkagem diretamente com a DLL, incorporando o código de carga e inicialização dos ponteiros diretamente, exatamente como é feito com um shared object no Linux… Basta informar o nome da dll na opção -l do linker (possivelmente informarndo o path na opção -L). Caso contrário, teríamos que criar uma biblioteca estática de importação (no Visual Studio seria um arquivo com extensão .lib, no Linux seria um archive, extensão .a). No caso do uso da biblioteca estática de importação, podemos criá-la através dos utilitários dlltool ou dllwrap, que acompanham o MinGW. Note que este é o comportamento default do Visual Studio: Toda DLL importada em tempo de linkagem tem que ter uma bibliocate de importação estática, criada pelo utilitário implib… Não é o caso do GCC.

Já no caso de usarmos late binding, temos que fazer algo assim, em código:

#include <windows.h>
#include <GL.h> /* Para usar GLenum. */

HMODULE hLib;

/* Ponteiro para uma função de opengl.dll. */
void CALLBACK (*glBegin)(GLenum);
...

/* Tenta carrgar opengl32.dll */
if (!(hLib = LoadLibrary("opengl32.dll"))
{ ... trata erro de carga aqui ... }

/* Obtem o ponteiro da função. */
glBegin = (void CALLBACK (*)(GLenum))GetProcAddress(hModule, "glBegin");
if (!glBegin)
{ ... trata erro de binding aqui ... }

...
/* Usa a função */
glBegin(GL_TRIANGLES);
...

/* Quando não precisar mais da bibliteca, livramo-nos dela: */
FreeLibrary(hLib);

É comum encontrarmos códigos de late binding, no Windows, que crie typedefs convenientes para o casting de ponteiros de funções. No exemplo acima você pode encontrar algo assim:

typedef void CALLBACK (*GLBEGINFARPROCPTR)(GLenum);

GLBEGINFARPROCPTR glBegin;
...
glBegin = (GLBEGINFARPROCPTR)GetProcAddress(hLib, "glBegin");
...

Fica mais “fácil” de ler, mas prefiro o casting explícito (já tive alguns problemas com typedefs)…

Outra coisa: Para descobrir os nomes reais das funções exportadas pela DLL, basta usar o utilitário objdump, que acompanha o MinGW:

C:\> objdump -x %windir%\system32\opengl32.dll | 
          sed -n '/Ordinal\/Name/,/^PE/p'
[Ordinal/Name Pointer] Table
        [   0] GlmfBeginGlsBlock
        [   1] GlmfCloseMetaFile
        [   2] GlmfEndGlsBlock
        [   3] GlmfEndPlayback
        [   4] GlmfInitPlayback
        [   5] GlmfPlayGlsRecord
        [   6] glAccum
        [   7] glAlphaFunc
        [   8] glAreTexturesResident
        [   9] glArrayElement
        [  10] glBegin
        ...
        [ 365] wglUseFontBitmapsW
        [ 366] wglUseFontOutlinesA
        [ 367] wglUseFontOutlinesW

PE File Base Relocations (interpreted .reloc section contents)
C:\>

(O ‘sed’ está em outra linha por motivos de legibilidade aqui. ‘\’ como continuação de linha parece não funcionar no Windows!)

Como falei lá em cima, no modo i386, provavelmente o ponteiro conterá o tamanho dos parâmetros quando usado como early binding e, portanto, será conhecido como __imp__glBegin@4 (e, de novo, o mesmo não acontece no modo x86-64 e com late binding).

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!

Não precisa “decorar”, mas é bom saber…

Alguns leitores já me pediram para explicar como uma instrução em assembly é codificada em “linguagem de máquina” e minha resposta é sempre “deixe isso por conta do compilador!”. Você realmente deveria fazer isso, mas conhecer a codificação, mesmo que superficialmente, pode ajudar a planejar ou, pelo menos, analisar o código gerado pelo compilador… Algumas instruções podem ser “maiores” (em bytes) do que outras e colocarem mais “pressão” no cache L1I do que deveriam. Arranjar instruções para melhor aproveitar o cache é algo desejável em qualquer programa.

Uma instrução é formada por um código de operação e uma série de parâmetros. Instruções que não tomam parâmetros explícitos costumam ter apenas 1 byte como código de operação. É o caso da instrução RET e RETF (retorno “far”), por exemplo, cujos códigos são 0xC3 e 0xCB, respectivamente. Mas outras instruções tomam registradores, ponteiros e valores imediatos. Como elas são codificadas?

O diagrama abaixo, retirado do Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 – Instruction Set Reference, mostra como:

instruction encoding

Antes do opcode podemos ter prefixos, falarei deles daqui a pouco. O opcode pode ter de 1 até 3 bytes, dependendo da instrução. Daí para frente os bytes adicionais dependem do opcode:

  • ModR/M e SiB têm 1 byte cada. Podemos ter um byte com ModR/M apenas, mas podemos ter um ModR/M e um SiB;
  • Displacement é o offset num esquema de endereçamento do tipo [base+índice*escala+offset]. Ele está presente, dependendo dos campos em ModR/M e SiB;
  • Immediate é um valor imediato, dependendo da instrução.

O termo ModR/M vem do fato de que esse byte especifica o “modo” de endereçamento, o registrador isolado (se o opcode exigir) e outro registrador ou ponteiro (Register/Memory)No caso de selecionada “Memory”, o byte SiB estará presente também. O byte SiB especifica a escala (Scale), o índice (index) e o registrador base (Base).

Vejamos um exemplo:

00000000: 89 D8          mov eax,ebx

Dando uma olhada no manual da Intel vemos que 0x89 corresponde a instrução “MOV r/m32,r32“. Essa instrução precisa de um registrador fixo (r32) e de um outro operando que pode ser um registrador ou um ponteiro. Isso é decidido no próximo byte, o ModR/M. O valor 0xD8, em binário, é 0b11011000 ou, separando os bits nos campos citados anteriormente temos: 0b11_011_000. De novo, consultando o manual vemos que se Mod for 0b11, Reg for 0b011 e R/M for 0, r32 será EBX e r/m32 será EAX.

É interessante notar que esse não é o único jeito de especificar a mesma instrução. O código 0x8B 0xC3 resulta na mesma instrução! Neste caso a instrução é “MOV r32,r/m32” e o campo ModR/M especifica os mesmos EAX e EBX, mas no sentido contrário!

Prefixos:

Vale lembrar que os processadores da família x86 foram criados para trabalharem com 16 bits. Com o surgimento do 386 o conjunto de instruções teve que ser expandido para suportar 32 bits e, mais tarde, com a AMD criando o modo x86-64, 64 bits (dã!).

Tanto no modo i386 quanto no modo x86-64 o processador espera encontrar operandos de 32 bits em suas instruções. No modo i386 ele também espera encontrar endereços de 32 bits, mas espera encontrar de 64 no modo x86-64. Dessa forma a Intel criou 2 prefixos especiais: 0x66 e 0x67. O prefixo 0x66 muda o sentido esperado de um operando (operand override), já o prefixo 0x67 muda o sentido esperado de um endereço ou ponteiro (address override). Assim, no modo i386, se usarmos AX ao invés de EAX como operando, o compilador colocará 0x66 indicando que estamos usando 16 bits ao invés de 32:

00000000: 66 89 D8       mov ax,bx

Repare que a instrução é exatamente a mesma que “MOV EAX,EBX“, exceto pelo prefixo 0x66. É necessário ter atensão a esse prefixo porque ele quer dizer “mudança do sentido esperado do operando”. No modo real (16 bits) o processador espera encontrar operandos de 16 bits, ao usar os registradores de 32 o prefixo 0x66 terá que ser usado. A mesma codificação acima significa “MOV EAX,EBX” no MS-DOS, por exemplo.

O prefixo 0x67 aplica-se aos ponteiros do tipo “[base+índice*escala+offset]”. No modo i386 espera-se que os registradores “base” e “índice” sejam de 32 bits, se a instrução for precedida de 0x67 eles serão de 16. No modo x86-64 espera-se que eles sejam de 64 bits e serão de 32 se o prefixo for usado. Eis um exemplo do modo x86-64:

00000000: 67 8B 04 33   mov eax,[ebx+esi]
00000004: 8B 04 33      mov eax,[rbx+rsi]

Lembre-se: Mesmo no modo x86-64 o processador espera encontrar operandos de 32 bits, não de 64. Mas isso não vale para ponteiros! No caso acima o prefixo 0x67 diz ao processador que interprete os bytes ModR/M e SiB como usando registradores de 32 bits. Se estivéssemos no modo i386 os mesmos bytes criariam instruções diferentes:

00000000: 67 8B 04      mov eax,[si]
00000003: 33            ???
00000004: 8B 04 33      mov eax,[ebx+esi]

Ué?! O que houve com a primeira instrução?! No modo 16 bits não podemos usar qualquer registrador como “base”. Podemos usar apenas BX, BP ou zero. O registrador de índice pode ser, em 16 bits, SI ou DI, apenas. Assim, prefixar a instrução “MOV EAX,[EBX+ESI]” com 0x67 não a faz, automaticamente, ser “MOV EAX,[BX+SI]“… Mesmo estando no modo i386 a mudança de semântica do endereço segue a do modo real.

Ao adicional 0x66, no entanto, mudará o contexto dos operandos que não são ponteiros se estivermos no modo i386. Por exemplo:

00000000: 8B 04 33      mov eax,[ebx+esi]
00000003: 66 8B 04 33   mov ax,[ebx+esi]

E no modo x86-64 a semântica é muito parecida, exceto que os ponteiros serão de 64 bits. Adicionando 0x67 ao prefixo 0x66 mudamos o endeerço também:

00000000: 8B 04 33        mov eax,[rbx+rsi]
00000003: 66 8B 04 33     mov ax,[rbx+rsi]
00000007: 66 67 8B 04 33  mov ax,[ebx+esi]

A ordem de 0x66 e 0x67 não importa.

O problemas dos prefixos:

Não importa a quantidade de vezes que esses prefixos sejam informados antes da instrução, exceto pelo fato de que uma única instrução (prefixada, inclusive) pode ter apenas até 15 bytes de tamanho. A instrução abaixo causará uma exceção porque tem 16 bytes de tamanho, mesmo que ela não faça nada:

00000000: 66 66 66 66 66 66 66 66
00000008: 66 66 66 66 66 66 66
0000000F: 90                       nop

Dito isso, é bom lembrar que além dos prefixos 0x66 e 0x67 existem outros, mais específicos. No modo x86-64 é possível termos instruções perfeitamente válidas, e sem o uso de gambiarras como essa ai acima, que venham a ter mais que 15 bytes graças ao uso (correto) de prefixos… Infelizmente não há exceções: Tem mais que 15 bytes, o processador não deixa executar!

Outros prefixos especiais:

No modo x86-64 temos um prefixo chamado de REX. Esse prefixo estende os campos dos bytes ModR/M e SiB, se usados pela instrução, colocando um bit extra. O prefixo possui forma binária de 0x0100WRXB, onde o bit W especifica o tamanho de um operando que não seja um ponteiro (1=64 bits, 0=32). O bit R estende em um bit o campo Reg de ModR/M possibilitando o uso dos outros 8 registradores estendidos (R8 até R15 e XMM8 até XMM15). O bit X faz o mesmo, mas para o campo index do SiB, assim como o bit B faz isso para o campo base.

Assim, uma instrução contendo um prefixo de 0x40 até 0x4F especifica registradores estendidos ou endereços que os usam.

Outro detalhe é que, se um prefixo REX existir ele deve preceder imediatamente o opcode. Antes de REX podemos ter 0x66 ou 0x67, mas não depois.

Além do prefixo REX temos outros mais tradicionais como REPNZ (e REP) e REPZ. Esses são usados com instruções de bloco como LODS, STOS, MOVS e SCAS, mas também podem ser usados com outras instruções e, neste caso, são inócuos… Servem apenas ao propósito de alinhamento. É comum encontrarmos códigos que contenham instruções como “REP RET“, onde esse REP está ai só para fazer RET usar 2 bytes.

Um outro prefixo especial, sem nenhum bitmask, é 0x0F. Ele especifica algumas instruções estendidas, que não estavam presentes nos processadores anteriores ao 486…. E, à medida que novos conjuntos de instruções foram sendo adicionados aos processadores, novos prefixos foram sendo usados, com seus próprios significados: É o caso de prefixos VEX e XOP, por exemplo. Não vou explicá-los agora, basta dizer que eles são usados em conjuntos de instruções de AVX e outras extensões do processador. O princípio é parecido com o prefixo REX

Exceções à regra:

Você pode achar estranho, mas usar registradores como AL e DL, por exemplo, não causam a adição de prefixos como 0x66, nem no modo i386, nem no x86-64. No entanto, ao usar registradores como R8B o compilador não terá alternativa senão inserir um prefixo REX. O mesmo para DIL, SIL, BPL e SPL.

GCC versus clang

Vejam porque prefiro o GCC ao famoso clang: Performance e código menos inchado! Para mostrar isso eis a mais simples das rotinas: strlcpy() e usarei o GCC 4.9.2 e o clang 3.5:

$ gcc --version
gcc (Debian 4.9.2-10) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ clang --version
Debian clang version 3.5.0-10 (tags/RELEASE_350/final) (based on LLVM 3.5.0)
Target: x86_64-pc-linux-gnu
Thread model: posix

Eis o código que será compilado pelos dois:

char *strlcpy(char *dest, char *src, size_t size)
{
  char *p = dest;
  while (*src && size--)
    *p++ = *src++;
  *p = '\0';
  return dest;
}

Eis os códigos equivalentes gerados pelos dois compiladores, ambos compilados com a chave “-O3” apenas:

;------ rotina gerada pelo gcc -------
  align 16
strlcpy:
  movzx ecx,byte [rsi]   ; ECX = *dest;

  mov   rax,rdi          ; RAX = dest
  mov   r8,rdi           ; R8 = dest

  test  cl,cl
  je    .strlcpy_exit

  test  rdx,rdx          ; size != 0? loop
  jne   .L2

  jmp   .strlcpy_exit    ;... senão sai.

  align 4
.loop:
  sub   rdx,1            ; size--;
  je    .strlcpy_exit    ; size == 0? sai!
.L2:  
  add   r8,1             ; dest++
  add   rsi,1            ; src++
  mov   [r8-1],cl        ; *(dest - 1) = cl
  movzx ecx,byte [rsi]   ; cl = *src;
  test  cl,cl            ; cl != 0? loop.
  jne   .loop
  
.strlcpy_exit:
  mov   byte [r8],0     ; *dest = '\0';
  ret

;------ rotina gerada pelo clang -------
  align 16
strlcpy:
  mov  r8b,[rsi]        ; R8B = *src;
  test r8b,r8b          
  mov  rcx,rdi          ; RCX = dest
  je   .strlcpy_exit    ; R8B == 0? sai
  test  rdx,rdx         ; size == 0? sai
  je   .strlcpy_exit

  mov   eax,1
  sub   rax,rdx         ; RAX = 1 - size;
  inc   rsi             ; src++;
  mov   rcx,rdi         ; RCX = dest (de novo?!)

  align 16
.loop:
  mov   [rcx],r8b       ; *dest = R8B
  inc   rcx             ; dest++;
  mov   r8b,[rsi]       ; R8B = *src;
  test  r8b,r8b         ; R8B == 0? sai.
  je    .strlcpy_exit
  inc   rsi             ; src++;
  test  eax,eax         
  lea   rax,[rax+1]     ; RAX = RAX+1
  jne   .loop           ; RAX != 0, loop

.strlcpy_exit:
  mov  byte [rcx],0     ; *dest = '\0';
  mov  rax,rdi          ; RAX = dest
  ret

Ok. Devo conceder que o clang é espertinho, especialmente com o tratamento do tamanho da string destino, mas note que o loop principal da rotina é mais lenta em comparação com a criada pelo GCC!

Ainda, o uso de ADD reg,1 ao invés de INC reg é uma dica da superioridade do GCC. A Intel recomenda isso porque INC, ao contrário de ADD, é uma daquelas instruções que leêm e depois escrevem e, com isso, a mudança parcial dos flags pode gastar um ciclo extra (INC não afeta CF! Se, por exemplo, houver overflow do valor sem sinal, então CF será 1, mas INC não o altera e, para isso, precisa manter uma cópia do antigo CF e alterá-lo de volta depois da operação aritmética).

Outra dica é que o GCC manteve o tamanho do loop em RDX intocado até a entrada no loop. Ele usa o valor passado para a função como contador. O clang, por outro lado, fez um esquema interessante: Ele faz RAX=-RDX+1 e vai incrementando RAX (que é negativo para RDX > 1) até que esse seja zero. Mas, isso quer dizer que RAX precisará ser recarregado com o ponteiro dest, no final do loop.

O compilador clang ainda deixou passar o fato dele já ter carregado RCX antes de entrar no loop e o recarrega uma segunda vez!!! Isso, em minha opinião, é uma séria indicação da má qualidade das rotinas de otimização do compilador ou, pelo menos, que clang não é tão “melhor” que o GCC quanto alguns querem crer… Dito isso, o GCC deixou passar uma ou duas coisinhas também:

  • Já que a compilação foi genérica, sem a especificação de arquitetura, então, por que diabos o GCC escolheu carregar CL, através da função MOVZX, alterando todos os 64 bits de RCX? Isso só acrescentará o prefixo 0x0F numa instruçãoq que é semelhante a mov cl,[rsi]. A performance de ambas as instruções seria a mesma (de mov cl,[rsi] e movzx ecx,byte [rsi]), mas a primeira tem 1 byte a menos, colocando ainda menos pressão no cache L1i;
  • Ao invés de saltar para o final da rotina caso RDX seja zero, o compilador resolveu fazer um salto condicional para o loop. Isso pode ser explicado graças ao alinhamento, mas um salto condicional para frente, deste jeito, faz com que a regra do algoritmo estático do branch prediction seja quebrada, causando penalidade na primeira iteração do loop. Isso é facilmente corrigível, mas o ponto é que o GCC perdeu essa oportunidade!

Outro ponto para o GCC é que ele criou rotina que não sofrerá com efeitos de cache miss, relação cache L1i. Dando uma olhada na imagem binária de ambas as rotinas:

strlcpy (GCC):
 0000 0fb60e48 89f84989 f884c974 244885d2  ...H..I....t$H..
 0010 750ceb1d 0f1f4000 4883ea01 74134983  u.....@.H...t.I.
 0020 c0014883 c6014188 48ff0fb6 0e84c975  ..H...A.H......u
 0030 e741c600 00c3                        .A....

strlcpy (clang):
 0000 448a0645 84c04889 f9743f48 85d24889  D..E..H..t?H..H.
 0010 f97437b8 01000000 4829d048 ffc64889  .t7.....H).H..H.
 0020 f9666666 6666662e 0f1f8400 00000000  .ffffff.........
 0030 44880148 ffc1448a 064584c0 740c48ff  D..H..D..E..t.H.
 0040 c64885c0 488d4001 75e6c601 004889f8  .H..H.@.u....H..
 0050 c3                                   .

Considerando o tamanho de uma linha de cache com 64 bytes, o bloco em vermelho (e roxo) na rotina gerada pelo clang encontra-se numa segunda linha do cache L1i. Se, por acaso, essa linha já não estiver presente, um cache miss ocorrerá. Obviamente isso não ocorrerá com o código gerado pelo GCC (a rotina inteira está numa única linha!).

Os bytes em verde (e roxo) correspondem ao loop de ambas rotinas e os bytes em preto a preparação para o loop. Os bytes em cinza correspondem ao espaço desperdiçados no alinhamento do loop… O GCC conseguiu fazer com que o alinhamento do início do loop não gastasse tantos bytes assim (apenas 4). Já o clang “gastou” 15 bytes (poderia ter gastado só 3!). Isso é estranho, porque mesmo que o compilador assumisse uma linha de cache de 32 bytes (arquiteturas “genéricas”, antigas) o início do loop estaria no final da segunda linha e haveria uma terceira. De qualquer maneira, se o processador tivesse uma linha de 32 bytes, o código do GCC poderia sofrer 1 cache miss na primeira iteração do loop, já o do clang, 2.

Pintei os bytes de roxo na listagem gerada pelo clang para mostrar que esse pedaço do loop está em outra linha de cache, de novo, considerando a linha com 64 bytes. As linhas em vermelho indicam essa segunda linha (roxo seria “verde” + “vermelho”).

Resumindo: O GCC tende a gerar código menor e mais rápido que o clang. Isso não significa que clang não possa gerar código melhor em algum caso (embora eu não conheça um!).

Porque os tipos size_t e ssize_t são importantes?

Já me perguntaram por que diabos esses tipos existem… Bem, a espeficação ISO os definem com sendo inteiros com o mesmo tamanho de um ponteiro. Isso simplifica o código gerado pelo compilador em alguns casos. Vejamos um exemplo:

#include <stdlib.h>

extern int x[];

int f(int i) { return x[i]; }
int g(size_t i) { return x[i]; }

O compilador tende a converter o índice i para o mesmo tamanho de um ponteiro, já que o símbolo x é, na verdade, o ponteiro para o primeiro elemento do array. E, já que size_t tem o mesmo tamanho de um ponteiro, a conversão não é necessária:

f:
  movsx rdi,edi     ; Converte 'int' para 'size_t'.
  mov   eax,[rdi*4] ; O índice é multiplicado por 4
                    ; porque um 'int' tem 4 bytes.
  ret

g:
  mov   eax,[rdi*4] ; O índice é multiplicado por 4
                    ; porque um 'int' tem 4 bytes.
  ret

Usar o tipo int como índice de arrays é prática corriqueira mas cria códigos menos que ótimos, a não ser que o tamanho de um int seja o mesmo que o tamanho de um ssize_t (Note o ‘s’ antes de ‘size_t’. O tipo ssize_t tem o mesmo tamanho que size_t, mas tem sinal!).

O uso de ssize_t pode, até mesmo, criar código bem mais performático por causa da ausência da conversão de tipos. Por exemplo:

; void f(int count)
; { while (count--) x[count] = 0; }
f:
  test  edi,edi
  je    .L1
  lea   eax,[rdi-1]   ; **
  movsx rdi,edi
  xor   esi,esi
  sal   rdi,2         ; !!
  lea   rdx,[4+rax*4] ; **
  sub   rdi,rdx       ; !!
  add   rdi,x         ; !!
  jmp   memset
.L1:
  ret

; void g(size_t count)
; { while (count--) x[count] = 0; }
g:
  test  rdi,rdi
  je    .L1
  lea   rdx,[rdi*4]   ; **
  xor   esi,esi
  mov   edi,x         ; !!
  jmp   memset
.L1:
  ret

As rotinas são exatamente as mesmas, a única diferença é o uso do índice… As instruções marcadas com ‘!!’ são as que o compilador usou para calcular o endereço efetivo do array x e as marcadas com ‘**’ são usadas para calcular o tamanho do bloco de memória que será preenchido por memset.

Viu só como o compilador lutou bravamente para converter int para size_t no código de f(), mas acabou com um código complicado?

Vale dizer que isso não tem nada a ver com size_t ser um tipo não sinalizado e int ter sinal. Se você substituir size_t por ssize_t obterá exatamente o mesmo código para a função g(). Isso tem haver com ponteiros!