strlen_sse42() usando função “intrínseca”.

Pode parecer que a instrução PCMPISTRI seja meio complicada de usar em C, já que ela oferece dois resultados diferentes: ECX, contendo um índice de acordo com a comparação, e os flags. Mas, felizmente, o valor de ECX será 16 se InRes2 estiver totalmente zerado! Assim, a função anterior, escrita em assembly, pode ser reescrita em C assim:

/* test.c

  Compilar com:
    gcc -Ofast -msse4.2 -o test test.c
*/
#include <stddef.h>
#include <x86intrin.h>

size_t strlen_sse42_c(const char *s)
{
  unsigned int index;
  size_t result;
  static const char ranges[16] = { 1, 255 };

  result = 0;
  do
  {
    index = _mm_cmpistri(*(__m128i *)ranges, 
                         *(__m128i *)s,
                         _SIDD_UBYTE_OPS         | 
                         _SIDD_CMP_RANGES        | 
                         _SIDD_NEGATIVE_POLARITY |
                         _SIDD_LEAST_SIGNIFICANT);

    result += index;
    s += sizeof(__m128i);
  } while (count == 16);

  return result;
}

O código final ficará semelhante, mas menos performático, ao anterior:

bits 64

section .rodata

  align 16
_ranges:  db 1, 255
          times 14 db 0

section .text

global strlen_sse42:
  align 16
strlen_sse42:
  movdqa xmm0,[_ranges]
  xor    eax,eax

.loop:
  pcmpistri xmm0, [rdi], 0x0_01_01_0_0  
  mov    edx,ecx
  add    rdi,16
  add    rax,rdx
  cmp    ecx,16
  jz     .loop
  
  ret

Algumas diferenças óbvias: a instrução MOVDQA é mais rápida que MOVDQU e exige que o array _ranges esteja alinhado. Eu deveria ter previsto isso no código em assembly no artigo anterior… O compilador escolheu fazer DUAS comparações, como instruído. Como não temos como verificar o flag ZF à partir da função intrínseca _mm_cmpistri, só nos restava comparar o valor retornado com 16.

Agora… é evidente que PCMPISTRI só está disponível se seu processador suportar SSE 4.2. Um método bem simples de usar essa função OU a função padrão do compilador é este:

#include <stddef.h>
#include <string.h>
#include <x86intrin.h>

// Daqui para frente, strlen será chamada por esse ponteiro!
size_t (*__strlen)(const char *);

static size_t strlen_sse42_c(const char *s)
{ ... }

// Esse atributo faz com que a função seja executada
// ANTES de main(). É interessante ter apenas uma dessas
// funções em seu programa, embora o atributo permita definir
// a ordem de execução...
static __attribute__((constructor)) void ctor(void)
{
  if (__builtin_cpu_supports("sse4.2"))
    __strlen = strlen_sse42_c;
  else
    __strlen = strlen;
}

As chamadas a __stlen, evidentemente, serão sempre indiretas, mas assim você garante a compatibilidade entre processadores ao usar a rotina. Além do mais, a quase totalidade das funções da libc são chamadas de forma indireta, já que localizam-se em libc6.so.

strlen() usando SSE 4.2

Há uns meses atrás (ou será que já faz um ano?) topei com uma dica do amigo Cooler, em seu blog, mostrando como comparar duas strings usando SSE 4.2. A rotina é realmente rápida porque compara 16 bytes de cada vez, ao invés de byte por byte ou, até mesmo, dword por dword (ou qword, se otimizarmos para o modo x86-64). Confesso que não tinha nenhuma intimidade com a instrução PCMPISTRI, disponibilizada pelo SSE 4.2 e fiquei maravilhado com o achado.

A rotina que ele publicou foi essa (publico aqui apenas a versão x86-64):

; Original code by Cooler_  c00f3r[at]gmail[dot]com
bits 64
section .text

global strcmp_sse42

; int strcmp_sse42(char *ptr1, char *ptr2);
strcmp_sse42:
  mov    rax,rdi
  sub    rax,rsi
  sub    rsi,16
   
strloop:
  add    rsi,16
  movdqu xmm0,[rsi]
  pcmpistri xmm0,[rsi+rax],0b0011000
  ja     strloop
  jc     blockmov
  xor    eax,eax
  ret
  
blockmov:
  add    rax,rsi    
  movzx  rax,byte [rax+rcx]
  movzx  rsi,byte [rsi+rcx]
  sub    rax,rsi    
  ret

O detalhe, é claro, está na instrução PCMPISTRI e no byte de controle… Ele torna a instrução bastante flexível e, ao mesmo tempo, difícil de entender… Eis uma outra rotina da libc que pode ser acelerada (de novo, para x86-64, mas, agora, com meus comentários):

bits 64

section .rodata
; Pares de bytes contendo faixas (ranges).
  align 16
_ranges:
  db 1, 0xff      ; Faixa entre (char)1 e (char)0xff.
  times 14 db 0   ; As demais "faixas" não são "usadas".

section .text

global strlen_sse42

; size_t strlen_sse42(const char* s1)
  align 16
strlen_sse42:
  xor ecx,ecx           ; Necessário. Garante que os bits
                        ; superiores, [63:5], de RCX 
                        ; estejam zerados.

  lea rax,[rdi-16]
  movdqu xmm0,[_ranges]

.loop:
  add rax,16

  ; Compara os 16 bytes do buffer apontado por RAX
  ; Com os pares de faixas em XMM0...
  ;
  ; PCMPISTRI é "Packed Compare Implicit-ending String 
  ; returning Index" ou algo assim... "implícito" 
  ; significa string terminada com '\0'.
  ;
  ; O byte de controle da instrução representa:
  ;
  ; +-+-+---+---+-+-+
  ; |0|0|0 1|1 0|0|0|-----------> 0 = char
  ; +-+-+---+---+---+             1 = short
  ;    |  |   |  |
  ;    |  |   |  +--------------> 0 = unsigned
  ;    |  |   |                   1 = signed
  ;    |  |   |
  ;    |  |   |                   00 = Equal any
  ;    |  |   |                   01 = "Ranges"
  ;    |  |   +-----------------> 10 = Equal each (string compare)
  ;    |  |                       11 = Equal ordered
  ;    |  |
  ;    |  |                       00 = Positive Polarity
  ;    |  +---------------------> 01 = Negative Polarity
  ;    |                          10 = Masked (+)
  ;    |                          11 = Masked (-)
  ;    |
  ;    +------------------------> 0 = Least significant index
  ;                               1 = Most significant index

  pcmpistri xmm0,[rax],0b0_01_01_0_0  ; LSB InRes2 offset; 
                                      ; InRes2=~InRes1; 
                                      ; Range compare; 
                                      ; unsigned char.

  jnz .loop   ; ZF=1 só se um dos bytes de [rax] for 0 
              ; (o 'i' do mnemônico pcmp(i)stri garante isso).
              ; ECX é índice (dos 16 bytes) onde achou o '\0'.
              ; (o 'i' de pcmpistr(i) garante isso).

  add rax,rcx

  lea rdx,[rdi]
  sub rax,rdx
  ret

Bem… Como é que PCMPISTRI funciona? Ela toma dois valores de 16 bytes (sim! bytes!) e os comparara de acordo com os bits de 0 a 3 do valor imediato, segundo o comentário que você pode ver acima. No caso de usarmos “unsigned char”, cada byte comparado setará ou zerará um bit de um registrador interno chamado InRes1. Depois que a comparação for feita, podemos inverter ou não o valor contido em InRes1 e colocá-lo em InRes2 – a isso é dado o nome de “polarização” (informada nos bits 4 e 5).

No caso da instrução PCMPISTRI, o bit 6 diz como o valor de ECX será calculado. Isso é feito fazendo uma varredura em InRes2 e, o primeiro bit setado, encontrado (do primeiro ao último ou vice-versa) é colocado em ECX. Se todos os bits estiverem zerados, ECX retornará 16 (será?), mas o flag ZF também estará zerado por causa do I no final do nome do mnemônico (ou seja, a instrução não achou um fim-de-string, um char ‘\0’).

Na rotina acima eu peço para PCMPISTRI comparar cada byte da string com uma faixa de bytes variando de 1 a 255. Os valores zerados na faixa nao contam porque PCMPISTRI irá parar a busca ao encontrar um ‘\0’, de qualquer maneira…

Só um aviso… existem instruções similares, como PCMPESTRI, onde o tamanho da string tem que ser informado no par EDX:EAX. Existem também PCMPISTRM e PCMPESTRM que retornam uma “máscara”, isto é, o próprio InRes2.

Outra dica importante: Essas instruções são feitas para lidar com strings e, por isso, a Intel não impõe a necessidade dos dados estarem alinhados em 16 bytes, como é comum com as instruções SSE. Alinhei _range acima só por vício mesmo… :)

Surpresa! Contar ciclos no ring0 não é tão bom assim!

Estive aqui testando um módulo para o kernel para medir os ciclos de clock gastos por uma função qualquer e comparando isso com a minha rotina para fazer o mesmo, no userspace. A rotina sob teste é simples:

/* test.c */

// Não vamos usar esse array fora daqui,
// por enquanto...
int a[100] = { 0 };

void f(void)
{
  for (int i = 1; i < 100; i++)
    a[i] = a[i-1] + 1;
}

No userspace basta usar as funções inline begin_tsc e end_tsc, como mostradas abaixo:

/* cycle_counting.h */

#ifdef __x86_64__
# define REGS1 "rbx", "rcx"
# define REGS2 "rax, "rbx", "rcx", "rdx"
#else
# ifdef __i386__
# define REGS1 "ebx", "ecx"
# define REGS2 "eax, "ebx", "ecx", "edx"
# else
#  error "Need x86-64 or i386 platforms to use cycle counting!"
# endif
#endif

unsigned long long __local_tsc;

inline void begin_tsc(void)
{
  unsigned int a, d;

  __asm__ __volatile__ ("xorl %%eax,%%eax\n"
                        "cpuid\n"
                        "rdtsc"
                        : "=a" (a), "=d" (d)
                        :: REGS1);

  __local_tsc = ((unsigned long long)d << 32) | a;
}

inline unsigned long long end_tsc(void)
{
  unsigned int a, d;

  __asm__ __volatile__ ("rdtscp\n"
                        "movl %%eax,%0\n"
                        "movl %%edx,%1\n"
                        "xorl %%eax,%%eax\n"
                        "cpuid"
                        : "=m" (a), "=m" (d)
                        :: REGS2);

  // Retorna a contagem de ciclos.
  return ((unsigned long long)d << 32) + a - __local_tsc;
}

Já para usá-las no kernelspace, temos que criar um módulo:

/* cyclemod.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/hardirq.h>
#include <linux/preempt.h>
#include "cycle_counting.h"

//extern int a[];
extern void f(void);

static int __init cyclecnt_start ( void )
{
  int i;
  unsigned long flags;
  unsigned long long c;

  printk ( KERN_INFO "Loading Cycle-Counting module...\n" );

  preempt_disable();
  raw_local_irq_save(flags);

  begin_tsc();
  f();
  c = end_tsc();

  raw_local_irq_restore(flags);
  preempt_enable();

  printk ( KERN_INFO "\tCycles: %llu\n", c);

  return 0;
}

static void __exit cyclecnt_end ( void )
{
  printk ( KERN_INFO "Goodbye Cycle-Counter.\n" );
}

module_init ( cyclecnt_start );
module_exit ( cyclecnt_end );

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Frederico L. Pissarra");
MODULE_DESCRIPTION("Cycle Counting Module");

Para compilar esse bicho, crie o makefile abaixo e simplesmente execute make. Você verá diversos arquivos, mas o módulo é o com extensão .ko (kernel object).

ccflags-y := -Ofast
obj-m := cyclescount.o
cyclescounter-objs := cyclemod.o test.o

KVERSION = $(shell uname -r)

all:
  make -C /lib/modules/$(KVERSION)/build M=$(PWD) modules

clean:
  make -C /lib/modules/$(KVERSION)/build M=$(PWD) clean

A diferença na rotina acima é que desabilitamos as interrupções para o processador local (provavelmente o kernel usa apenas a instrução CLI) e a preempção (não haverá chaveamento de tarefa para essa thread)… Isso deveria eliminar algum custo que existe no userspace e nos dar uma contagem mais “precisa”, mas veja só… Não é isso o que acontece:

$ ./cnttest    # userspace app
Cycles: 552
$ sudo insmod cyclecounter.ko
$ sudo rmmod cyclecounter
$ dmesg | sed -n '/Load.\+Cycle-Count/,+2p'
[17630.739459] Loading Cycle-Counting module...
[17630.739464] 	Cycles: 748
[17630.747001] Goodbye Cycle-Counter.

Como assim, no userspace obtive uma contagem menor de ciclos do que no kernspace?

E agora, Zé?!

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ória… 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!