Mais um exemplo: Nem sempre assembly é a resposta!

Já escrevi sobre isso aqui: Criar suas próprias rotinas em assembly não é uma panaceia para a obtenção de alta performance. É possível acabar com uma rotina pior do que a gerada pela linguagem de alto nível. Especialmente se estivermos falando de compiladores que oferecem recursos de otimização.

Para demonstrar, consideremos a simples rotina de selection sort, abaixo:

#define swap(a,b) \
    { int _t; _t = (a); (a) = (b); (b) = _t; }

// Para ser justo, não vou permitir inlining...
__attribute__((noinline))
static void selection_sort(int *p, unsigned int size)
{
  unsigned int i, j;

  for (i = 0; i < size - 1; i++)
    for (j = i+1; j < size; j++)
      if (p[i] > p[j])
        swap(p[i], p[j]);
}

É legítimo pensar que essa rotina seja menos que ótima, já que usa o macro swap(), que lança mão de uma variável temporária que talvez seja alocada em memória. Assim, o programador pode criar uma rotina equivalente, em assembly, assim:

bits 64

section .text

; void selection_sort_asm(int *, unsigned int);
; -- Para ser justo, alinho em DWORD o ponto de entrada
;    da rotina!
  global selection_sort_asm
  align 4
selection_sort_asm:
  lea r8d,[rsi-1] ; r8d == size - 1;

  xor ecx,ecx     ; i = 0;

.loop2:
  lea edx,[rcx+1] ; j = i + 1;

.loop1:
  mov eax,[rdi+rcx*4]   ; if (ptr[i] > ptr[j]) ...
  cmp eax,[rdi+rdx*4]
  jle .skip

  xchg eax,[rdi+rdx*4]  ; ... swap(p[i], p[j]);
  mov [rdi+rcx*4],eax

.skip:
  add edx,1     ; j++
  cmp edx,esi   ; if (j < size) goto loop1;
  jb  .loop1

  add ecx,1     ; i++;
  cmp ecx,r8d   ; if (i < size - 1) goto loop2;
  jb  .loop2

  ret

Claro que essa rotina pode ser melhorada, especialmente entre os labels .loop1 e .skip, mas, em arquiteturas mais modernas (Ivy Bridge, SandyBridge e Haswell, sem contar com as mais recentes Broadwell e Skylake) os cálculo de endereços efetivos podem ser cacheados, numa espécie de common subexpression elimination, ou seja, isso não tem grande influência… Além do que, quero mostrar como uma abordagem “ingênua” se parece…

Bem… a rotina acima não é tão “ingênua” assim porque levei em conta algumas possíveis otimizações: Note que usei a instrução XCHG, usada para aproveitar o conteúdo de EAX na “troca” e evitar o uso de um outro registrador temporário. Repare também que não uso a instrução INC para evitar a penalidade de 1 ciclo de clock pelo fato dessa instrução não afetar o flag de carry (e precisar preservá-lo). É preferível usar ADD, mesmo tendo tamanho maior, em seu microcódigo… Ainda, os saltos condicionais são feitos “para trás”, para aproveitar o algoritmo estático de branch prediction (exceto para o label .skip). Uso também as instruções LEA para adições rápidas, carryless

Mas, se você medir a performance de ambas as rotinas contra um array de 10 elementos aleatórios obterá (num i5-3570 @ 3.4 GHz, arquitetura Ivy Bridge) algo em torno de 1800 ciclos para a rotina em C e 2200 ciclos para a rotina em assembly! Ou seja, a rotina em C é 18% mais rápida!!! Num i7, com arquitetura Haswell poderá obter menos ciclos…

A rotina em Assembly equivalente para o selection sort, gerada pelo GCC com as opções de compilação “-O2 -mtune=native“, é esta:

bits 64

section .text

  global selection_sort
  align 4
selection_sort:
  cmp esi, 1
  jbe .exit
  lea r11d, [rsi-1]
  mov r9, rdi
  xor r10d, r10d

  align 4
.loop2:
  add r10d, 1
  mov eax, r10d
  cmp esi, r10d
  jbe .skip2

  align 4
.loop1:
  mov edx, eax
  mov ecx, [r9]
  lea rdx, [rdi+rdx*4]
  mov r8d, [rdx]
  cmp ecx, r8d
  jle .skip1
  mov [r9], r8d
  mov [rdx], ecx

.skip1:
  add eax, 1
  cmp esi, eax
  jne .loop1

.skip2:
  add r9, 4
  cmp r10d, r11d
  jne .loop2
  ret

.exit:
  ret

Essencialmente, ela é a mesma coisa da rotina em assembly que mostrei acima. A diferença está na organização das instruções e no aproveitamento do paralelismo das unidades de execução do processador (não confundir com os “cores” ou “núcleos”!).

O GCC também evita instruções problemáticas como XCHG (que, quando faz acesso à memória, “trava” [lock] o barramento de dados). O compilador também entende que pontos de retorno de loops podem precisar estar alinhados para que eles não cruzem a fronteira de uma linha do cache L1I e, por isso, coloca aquele “align 4” lá… Ele também tenta evitar problemas de “interlocking” de registradores (quando modificamos um registrador e tentamos usá-lo, na instrução seguinte, como operando fonte. Isso evita que duas instruções sejam executadas em paralelo).

Ou seja, o compilador tenta levar em conta a grande quantidade de regras de otimização (que podem ser estudadas no manual de otimização de software da Intel [baixe aqui] – prepare-se: são quase 700 páginas!) que você, ou eu, poderíamos deixar “passar batido”. Isso não significa que não seja possível para você ou eu elaborarmos uma rotina que seja melhor da gerada pelo compilador. Isso significa, porém, que na maioria das vezes, o compilador faz um trabalho melhor do que faríamos!

Anúncios

Um kbhit() para Windows

Num artigo anterior (este aqui) mostrei como codificar um kbhit() para Linux. Ficou faltando a mesma função para Windows.

Algumas pessoas insistem em usar o header conio.h, que existe para o MinGW e o Visual Studio… Se esses são seus alvos para compilação e seu projeto não precisa ser portável, vá em frente, use o header. Mas, saiba que você pode codificar essas funções de forma mais “portável”, juntando as definições dos outros artigos (para Linux) com essas aqui.

No caso do Windows, podemos usar as funções da Console API, que é mais ou menos equivalente à termios, do Linux. Eis as rotinas _kbhit() e _getch():

#include <windows.h>

// Ok... essa função só tem um problema.
// Por causa do loop, a thread consumirá
// muito tempo de CPU...
int _kbhit(void)
{
  HANDLE hConsole;
  INPUT_RECORD ir;
  DWORD n;

  hConsole = GetStdHandle(STD_INPUT_HANDLE);
  while (PeekConsoleInput(hConsole, &ir, 1, &n))
    if (ir.EventType == KEY_EVENT)
      return 1;

  return 0;
}

int _getch(void)
{
  HANDLE hConsole;
  DWORD cm, n;
  char buffer[4];   // Para ter certeza que um caracter
                    // UNICODE cabe aqui, alocamos 4 bytes.

  hConsole = GetStdHandle(STD_INPUT_HANDLE);
  GetConsoleMode(hConsole, &cm);
  SetConsoleMode(hConsole, cm &
             ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT));
  ReadConsole(hConsole, buffer, 1, &n, NULL);
  SetConsoleMode(hConsole, cm);

  // Só preciso de 1 char...
  return buffer[0];
}

Repare como, em _getch() desabilito “LINE INPUT” (mais ou menos como o método “canônico” do termios) e o “ECHO INPUT”. Fiz algo semelhante no getch() e kbhit() lá do Linux…

A função _kbhit() fica um pouco mais simples por causa de PeekConsoleInput, que nos permite obter os eventos de entrada sem retirá-los da fila. Essencialmente, percorremos a fila de eventos à procura de um KEY_INPUT. Se algum for achado, retornamos 1 (true)…

Outra vez: Arrays e ponteiros…

Recentemente topei com a seguinte dúvida de um leitor: “Se um item de um array é referenciado por seu ponteiro-base e um offset, uma matriz é um array de ponteiros?”. Infelizmente, não! Para exemplificar, eis o que eu disse, até agora sobre arrays:

Um array e um ponteiro

No caso acima, o ponteiro p aponta para o primeiro item do array a. Note que a variável p contém, em seu interior, o endereço deste primeiro item. Isso significa que se derreferenciarmos o ponteiro p (com o operador de indireção *), obteremos o conteúdo de a[0]. Ou seja: a[0] = *(p+0) = *(a+0). Mas, note o que acontece com um array de ponteiros:

array de ponteiros para char

Cada ítem de a é, em si, um ponteiro para um bloco de chars. Agora, eis a diferença entre isso e um array bidimensional:

Array bidimensional

O símbolo a continua sendo um ponteiro para o primeiro item do array, mas a estrutura, na memória, é a de um array unidimensional simples, onde cada linha tem 9 chars. Os caracteres adicionais, em cada linhas, que não foram preenchidos são automaticamente preenchidos com zeros.

Repare que, na notação da linguagem C, podemos especificar um array “incompleto”, mas somente na última dimensão (a mais a esquerda). Isso é assim porque o compilador precisa saber qual é o tamanho de cada “linha” da “matriz” para calcular a posição inicial da linha desejada… No caso, a especificação a[1][4] é calculada como a+9\cdot1+3, ou, de forma mais genérica:

\displaystyle a[l][c] = *(a + l\cdot C+c)

Onde C é a quantidade de colunas que uma linha possui e as variáveis minúsculas l (letra “éle”) e c correspondem a linha e coluna desejadas. Precisamos multiplicar a linha desejada pelo número de colunas que o array tem e depois adicionar a coluna desejada para obter o offset, em relação ao ponteiro-base a. Note que, em ambos os casos, a especificação a[1][4] nos dará o caracter ‘a’, que está na 13ª posição do array unidimensional equivalente.

Mas, como isso fica se tivermos um array de 3 ou mais dimensões?  Para tornar o cálculo mais simples, especificarei um array “tridimensional” assim: T a[Z][Y][X], onde T é um tipo qualquer e (X,Y,Z) são os tamanhos de cada dimensão. De novo, usarei (x,y,z), minúsculos, para especificar uma posição desejada…

Em primeiro lugar, o tamanho desse array é de X\cdot Y\cdot Z\cdot sizeof(T) bytes e, para encontrarmos o início do array bidimensional especificado por z, precisamos calcular o offset assim: z\cdot X\cdot Y, porque cada uma dessas “camadas” têm X\cdot Y de tamanho. De maneira similar ao array bidimensional, para achar uma coluna nessa “camada”, precisamos fazer y\cdot X+x. Assim, a posição (x,y,z) do array a é derreferenciada como a[z][y][x] = *(a + X\cdot Y\cdot z + X\cdot y + x).

Na notação de arrays, o compilador faz esses cálculos pra você… mas, é bom lembrar que isso é completamente diferente de usar três indireções.

Ahhh. Uma dica: Se for criar arrays multidimensionais, tenha certeza de que as dimensões inferiores tenham tamanho múltiplo de 2^n. Isso porque, já que o compilador terá que usar multiplicações para calcular o offset, dessa maneira ele usará apenas shifts lógicos. Acelera um bocado… Já a dimensão de alta ordem, que não é usada no cálculo do offset como fator multiplicativo, pode ser de qualquer tamanho… Assim, criar um array int a[10][10] é bem menos performático do que criar um int a[10][16]. Você gasta um pouco mais de espaço, mas a performance melhora um bocado.

O gráfico animado dos sorteios (howto)

Alguns poucos leitores me perguntaram como foi que eu fiz aquele gráfico com os pontos “subindo” a cada 100 ms… Bem… existem várias formas de fazer. Delas, vou mostrar a que usei e explicar sobre uma alternativa:

Podemos criar uma série de imagens e depois sequenciá-las usando o ffmpeg. Essas imagens podem ser criadas num formato puramente textual, chamado PPM. O programinha que lê cada um dos sorteios e gera as imagens pode ser escrito assim:

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

/* WIDTH e HEIGHT */
#define W 60
#define H 228

static unsigned char buffer[H][W];
static int accum[61] = {};

static void draw(FILE *);

int main(void)
{
  int n, i;
  int v[6];
  char path[16];
  FILE *fin, *fout;

  if (!(fin = fopen("sorteios.lst", "rt")))
  {
    fputs("Erro tetando ler o arquivo com os sorteios!\n",
          stderr);
    return EXIT_FAILURE;
  }

  for (n=1;;n++)
  {
    // Se não conseguiu ler uma linha completa,
    // não temos mais linhas para ler.
    if (fscanf(fin, "%d %d %d %d %d %d",
          v, v+1, v+2, v+3, v+4, v+5) != 6)
      break;

    // Calcula a acumulação...
    for (i = 0; i < 6; i++)
      accum[v[i]]++;

    // Este será o nome do novo arquivo.
    sprintf(path, "%05d.ppm", n);

    // Cria o arquivo, preenche o "frame buffer"
    // e cria o arquivo PPM.
    printf("Amostra %d\r", n);
    fout = fopen(path, "wt");
    draw(fout);
    fclose(fout);
  }

  putchar('\n');
  fclose(fin);

  return EXIT_SUCCESS;
}

void draw(FILE *f)
{
  int x, y;

  memset(buffer, 255, sizeof buffer); // fundo branco.
  for (x = 0; x < 60; x++)
    buffer[226 - accum[x+1]][x] = 0;  // pixel preto.

  // Header to arquivo PPM.
  fprintf(f, "P3\n%d %d\n255\n", W, H);
  for (y = 0; y < H; y++)
  {
    for (x = 0; x < W; x++)
      if (buffer[y][x])
        fputs("255 255 255 ", f);
      else
        fputs("0 0 0 ", f);

    fputc('\n', f);
  }
}

Claro, eu escalonei um cadinho mais para ficar mais “visível”, mas, em essência, é esse código ai…

Depois desse programinha teremos 1960 arquivos, nomeados de 00001.ppm até 01960.ppm, bastando, agora, usar o ffmpeg para juntá-los e criar a animação. Jogaremos os arquivos PPM no lixo, depois disso:

$ ffmpeg -i %05d.ppm -qmax 11 -r 10 anim.gif && rm *.ppm

A outra maneira de criar a animação é usando uma biblioteca dedicada. Por exemplo, libgd. No caso, cada “quadro” é formado preenchendo um retângulo branco em todo o frame e plotando os pixels nas coordenadas correspondentes, mais ou menos como fiz na rotina acima, só que usando gdImageFilledRectangle e gdImageSetPixel.

A biblioteca libgd já grava a saída diretamente num arquivo GIF. Tudo o que devemos fazer é criar uma imagem com gdImageCreate, iniciar uma animação com a função gdImageGifAnimBegin e ir adicionando os quadros com gdImageGifAnimAdd… No final dos 1960 quadros chamamos gdImageGifAnimEnd e, antes de fechar o stream de saída, gdImageGif, para terminar o processo… Ahhh, o delay, em milissegundos, é ajustado em gdImageGifAnimAdd

OBS: A função gdImageGifAnimAdd permite o uso de uma referência ao quadro anterior, para tentar realizar quadros “diferenciais”, tornando o GIF menor. Isso exige que o quadro anterior seja “clonado” antes de criarmos o novo quadro.

A vantagem de usar libgd é que não temos que nos preocupar com a degradação da qualidade da imagem que fatalmente ocorrerá ao usarmos o ffmpeg para converter os vários PPM para GIF (qualidade que tentei manter ao forçar a barra com a opção -qmax 11).

E, se você está curioso… eu usei o primeiro método porque não estava com paciência de depurar o programinha, usando libgd

Estatisticamente insignificante…

Well… só para escrever um cadinho a respeito de alguma coisa, recentemente alguém em um grupo de Facebook que participo teve a brilhante ideia de “elaborar um algoritmo” para ganhar na MEGA SENA. Como se ninguém tivesse pensado (e ainda pensando) nisso antes, né?

Muito bem… saibam que todos os jogos já sorteados estão disponíveis para download no site da Caixa Econômica Federal neste, link, em formato ZIP que contém um arquivo HTML, mal formatado, por sinal… Baixado os resultados por ordem de sorteio (aqui) e depois de alguma filtragem, podemos obter algo assim:

04 05 30 33 41 52
09 37 39 41 43 49
10 11 29 30 36 47
01 05 06 27 42 59
01 02 06 16 19 46
07 13 19 22 40 47
...

Essa é a lista de 1960 jogos já sorteados até o último jogo antes que eu tivesse criado esse texto. E, agora, podemos brincar, não é? Infelizmente, não…

O problema é que a quantidade de sorteios feita é insignificante em relação à quantidade de jogos possíveis. Só para dar uma ideia, a quantidade de combinações possíveis é de C(n,r)=\frac{n!}{r!(n-r)!}\Rightarrow\frac{60!}{6!(60-6)!}\approx50063860. Sabendo que temos apenas 1960 jogos disponíveis para fazer alguma avaliação, isso significa que temos apenas 0,003915% de todas as amostras possíveis! Tentar divisar algum padrão nessa base é como dizer que só existem cavalos marrons porque você nunca viu um com pelagem de outra cor…

Com base nas amostras que temos, podemos chegar as mais variadas conclusões falsas. Por exemplo, se traçarmos um gráfico com o número de ocorrências de cada um dos valores, em todos os 1960 jogos, teremos:

Distribuição dos valores em todos os sorteios

Olhando para isso podemos concluir (errado!) que deveríamos ignorar os valores 26 e 55, já que eles aconteceram bem menos do que os demais. Ou, quem sabe, deveríamos prestar mais atenção justamente neles, já que eles tendem a acontecer mais, para manter o “sistema” equilibrado (errado! errado!).

Ainda, existem 6 valores que aconteceram mais que os demais (5, 10, 23, 33, 51 e 53), então, essa deve ser uma sequência mais “apostável” que as demais, não é? E os números 5 e 53 aconteceram 225 vezes nesses 1960 jogos (11% de todos os jogos, pena que não ao mesmo tempo! — de fato, ao mesmo tempo, eles aconteceram apenas 17 vezes!).

De novo, esses dados são estatisticamente insignificantes para que qualquer tipo de análise, no sentido de obtenção de padrões, seja possível e uma outra demonstração disso é uma animaçãozinha que mostra a distribuição dos valores no tempo, onde cada linha contém uma dezena de valores (a linha 1 vai de 1 até 10, a linha 2, de 11 até 20… até a 6ª linha, de 51 até 60):

1/10º de segundo por sorteio

Repare como os quadrados vão ficando rosados, mais ou menos, ao mesmo tempo e mais ou menos terminam em tonalidades similares (e você, muitas vezes, nem percebe o degrau de variação nessa animação de 10 frames por segundo! Ou seja, 1 segundo = 10 sorteios). Isso significa que a distribuição é mais ou menos uniforme no decorrer do tempo, o que torna a “adivinhação” difícil!

Outra forma de ver a variação de cada valor, acima, é a simulação do gráfico inicial em timelapse (de novo, 10 frames por segundo):

Cada ponto é a contagem do valor em uso em cada sorteio

Ok… tem um pequeno bug no programinha que fiz para criar essa animação (a barra azul do lado direito), mas você pegou a ideia…

E, se você ainda acha, que dá para obter algum padrão, pense nisso: Será que o peso da tinta dos números pintados nas bolinhas sorteadas não têm alguma influência? E quanto à quantidade de plástico usado na fabricação delas? E a “qualidade” desse material? Todas as bolinhas vieram do mesmo fabricante? E aquela “cesta” giratória (se é que ainda usam isso!), de que material é feito? Ela parece ser metálica, existem soldas? Elas são uniformes? Tanto a cesta quanto as bolinhas são perfeitamente esféricas? E quanto ao atrito? E a temperatura ambiente? E o diferencial de temperatura entre a “cesta” (de metal) e as bolinhas (de plástico)? E quem está girando as cestas? Tá com fome? Tá cansado? Tá doente? etc…

Existem muitas variáveis e poucas amostras para dizer sequer se essas variáveis devem ou não serem consideradas!

Teimosia…

Tem gente teimosa que insiste em fazer as coisas da forma errada… Neste artigo eu informei a meus leitores, explicitamente, que a string que deve ser passada para a função setlocale segue os padrões ISO 639 e ISO 3166. Alguns acreditam que isso só vale para ambientes Unix e que no Windows a coisa é “mais fácil”… A insistência, é claro, é pela passagem de strings como “Portuguese” ou “Portugues”, como em:

// Isto está ERRADO!!!
setlocale(LC_ALL, "Portuguese");

A função setlocale(), como descrita na MSDN Library (aqui), diz claramente: “The set of locale names supported by setlocale are described in Locale Names, Languages, and Country/Region Strings.” e o tópico em questão nos diz: “The locale name form—for example, en-US for English (United States) or bs-Cyrl-BA for Bosnian (Cyrillic, Bosnia and Herzegovina)—is preferred“.

Mais um exemplo para comprovar que, mesmo o Windows, usa o padrão para locale, é usar uma função da Win32 API para obter o locale padrão do sistema:

#include <stdio.h>
#include <wchar.h>
#include <windows.h>

void main(void)
{
  wchar_t locale[LOCALE_NAME_MAX_LENGTH+1];

  // Usando Win32 API para obter o locale padrão!
  // GetSystemDefaultLocaleName() exige um ponteiro
  // para wide string.
  GetSystemDefaultLocaleName(locale, 
                             LOCALE_NAME_MAX_LENGTH);
  wprintf(L"%ls\n", locale);
}

Ao compilar e executar o programinha acima, usando o MinGW64, obtemos:

$ x86_64-w64-mingw32-gcc -O2 -o locale.exe locale.c
locales.c: In function ‘main’:
locales.c:8:3: warning: implicit declaration of function ‘GetSystemDefaultLocaleName’ [-Wimplicit-function-declaration]
   GetSystemDefaultLocaleName(locale, LOCALE_NAME_MAX_LENGTH);
   ^
... Copiando o locale.exe para o Windows e executando:
C:\Work> locale
pt_BR

Yep… tem esse aviso ai (provavelmente estou esquecendo de definir algum símbolo para compilar código para versão do Windows superior ao Vista), mas o linker não reclamou, então ele conseguiu resolver a referência corretamente…

Note que a própria Win32 API prefere o padrão ISO!

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

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

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

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

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

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

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

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

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

bits 16

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

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

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

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

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

bits 16

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

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

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

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

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

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

hextbl:
  db    '0123456789abcdef'

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

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

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

dword2hex:
  cvt_upper_byte
  cvt_upper_byte
  cvt_upper_byte
  cvt_upper_byte
  ret

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

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

Compile e execute com:

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

O resultado será este:

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

Outro detalhe são as chamadas. Vejamos:

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

Isso criará um código assim:

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

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

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

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

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

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

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

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

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

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