Para quem está criando o próprio “toy OS”…

Conheço algumas pessoas que estão experimentando a criação do próprio sistema operacional de brinquedo. Já vi alguns projetos interessantes, a maioria voltada para o modo protegido i386 (32 bits). Aqui vai uma dica para quem está usando o GCC para gerar código freestanding

A convenção de chamada padrão da linguagem C é chamada de cdecl, onde os argumentos de uma função são sempre empilhadas, na ordem do último argumento para o primeiro; e a função chamadora é a responsável por “limpar” a pilha dos argumentos. Acontece que ficar usando pilha para passagem de argumentos é um jeito bem lento de se fazer as coisas. Será que não tem alguma forma de usar registradores? Felizmente, tem! Antes, vamos dar uma olhada nas funções abaixo:

__attribute__((noinline))
int f(int a, int b, int c) 
{ return a * b + c; }

__attribute__((noinline))
int g(int a, int b, int c, int d, int e) 
{ return ( f(a, b, c) - d) / e; }

Se você compilar com as opções tradicionais, obterá algo assim:

; Código equivalente, para NASM, compilado com
; o GCC via:
;
; $ cc -O2 -S -masm=intel -ffreestanding -m32 test.c
;
  bits 32
  section .text

; A estrutura da pilha após a chamada de f é:

;   ESP+0:  Endereço de retorno colocado pelo CALL.
;   ESP+4:  Valor de 'a';
;   ESP+8:  Valor de 'b':
;   ESP+12: Valor de 'c';
;
  global f
f:
  mov  eax,[esp+8]
  imul eax,[esp+4]
  add  eax,[esp+12]
  ret

; A estrutura da pilha após a chamada de g é:
;
; ESP+0:  Endereço de retorno colocado pelo CALL;
; ESP+4:  Valor de 'a';
; ESP+8:  Valor de 'b';
; ESP+12: Valor de 'c';
; ESP+16: Valor de 'd';
; ESP+20: Valor de 'e';
;
  global g
g:
  ; Aqui tem um "macete"... Como cada PUSH
  ; decrementará ESP em 4, para acessar a próxima referência
  ; à pilha, temos que usar o mesmo offset "ESP+12".
  push	dword [esp+12]  ; Empilha 'c'.
  push	dword [esp+12]  ; Empilha 'b'.
  push	dword [esp+12]  ; Empilha 'a'.
  call	f
  add   esp, 12         ; Livra-se dos 12 bytes empilhados.

  sub   eax, [esp+16]
  cdq
  idiv  dword [esp+20]
  ret

Cada referência à memória consome, pelo menos, 1 ciclo de clock adicional para a instrução e, ainda, aumenta um bocado o tamanho do código. Eis o micro código das duas funções, obtida com o objdump:

f:
   0: 8b 44 24 08      mov  eax,[esp+0x8]
   4: 0f af 44 24 04   imul eax,[esp+0x4]
   9: 03 44 24 0c      add  eax,[esp+0xc]
   d: c3               ret

g:
  10: ff 74 24 0c      push DWORD PTR [esp+0xc]
  14: ff 74 24 0c      push DWORD PTR [esp+0xc]
  18: ff 74 24 0c      push DWORD PTR [esp+0xc]
  1c: e8 fc ff ff ff   call f
  21: 83 c4 0c         add  esp,0xc
  24: 2b 44 24 10      sub  eax,[esp+0x10]
  28: 99               cdq  
  29: f7 7c 24 14      idiv DWORD PTR [esp+0x14]
  2d: c3               ret

Agora, vejamos como fica o mesmo código, compilado com o GCC, mas incluindo a opção -mregparm=3:

f:
   0:	0f af d0         imul edx,eax
   3:	8d 04 0a         lea  eax,[edx+ecx*1]
   6:	c3               ret  

g:
  10:	e8 fc ff ff ff   call f
  15:	2b 44 24 04      sub  eax,[esp+0x4]
  19:	99               cdq  
  1a:	f7 7c 24 08      idiv DWORD PTR [esp+0x8]
  1e:	c3               ret    

O que aconteceu aqui é que o atributo regparm faz com que os 3 primeiros argumentos, se forem inteiros, são passados sempre pelos registradores EAX, EDX e ECX, nessa ordem. Os demais argumentos são empilhados, de acordo com a convenção de chamada cdecl. Na função g, apenas os argumentos d e e estarão empilhados e, como e é empilhado primeiro, ele estará em [ESP+8], deixando d em [ESP+4].

Note que, como EAX, EDX e ECX já contém os argumentos que serão passados para f, na função g, nenhum empilhamento é necessário, retirando os 3 PUSHs que existiam antes, bem como a limpeza da pilha. Do lado da função f, já que os argumentos estão todos em registradores, podemos usar LEA para fazer a adição final…

Bem… o resultado final é que as duas funções ficarão bem mais rápidas e com a metade do tamanho das originais!

Anúncios