Programa puramente escrito em C ou Assembly: Vale a pena?

Vale a pena escrever um programa puramente em assembly? Eis um exemplo simples do mesmo programa escrito em ambas as linguagens para o modo x86-64, no Linux: O objetivo é obter o conteúdo dos registradores seletores de segmentos CS, SS, DS, ES, FS e GS…

/* getregs.c */
/* Compilar com:
     $ gcc -O3 -mtune=native -s -o getregs getregs.c
*/
#include <stdio.h>

#define GET_SELECTOR(sel) \
  __asm__ __volatile__ ( \
    "movw %%" #sel ",%%ax\n" \
     : "=a" ( sel ) \
     );

void main(void)
{
  unsigned short cs, ss, ds, es, fs, gs;

  GET_SELECTOR(cs);
  GET_SELECTOR(ss);
  GET_SELECTOR(ds);
  GET_SELECTOR(es);
  GET_SELECTOR(fs);
  GET_SELECTOR(gs);

  printf("CS=0x%04X, SS=0x%04X, DS=0x%04X,  ES=0x%04X, FS=0x%04X, GS=0x%04X\n"
         "Base addr: 0x%016llX\n",
      cs, ss, ds, es, fs, gs,
      (unsigned long)main);
}

Eis o mesmo programa em assembly puro, escrito com o NASM, usando syscalls:

; getregs2.asm
;
; Compilar com:
;   $ nasm -felf64 -o getregs2.o getregs2.asm
;   $ ld -s getregs2.o -o getregs2
;
bits 64

;----------
; MACROS e DEFINIÇÕES
;----------
STDOUT_FILENO equ 1

; Usa a rotina local _puts.
%macro puts 2
  mov   rsi,%1
  mov   edx,%2
  call  _puts
%endmacro

%macro exit0 0
  xor   edi,edi
  mov   eax,60
  syscall
  hlt
%endmacro

section .rodata

  cs_msg:     db  "CS="
  cs_msg_len  equ $ - cs_msg
  ss_msg:     db  ", SS="
  ss_msg_len  equ $ - ss_msg
  ds_msg:     db  ", DS="
  ds_msg_len  equ $ - ds_msg
  es_msg:     db  ", ES="
  es_msg_len  equ $ - es_msg
  fs_msg:     db  ", FS="
  fs_msg_len  equ $ - fs_msg
  gs_msg:     db  ", GS="
  gs_msg_len  equ $ - gs_msg
  baseaddr:   db  "Base addr: "
  baseaddr_len  equ $ - baseaddr

  linefeed:   db  0x0a
  hexvals:    db  "0123456789ABCDEF"

section .data

  qascii:     db  "0x0000000000000000"
  qascii_len  equ $ - qascii

  wascii:     db  "0x0000"
  wascii_len  equ $ - wascii

section .text

  global  _start:function
  _start:
    puts  cs_msg, cs_msg_len

    mov   ax,cs
    call  w2a    
    puts  wascii, wascii_len

    puts  ss_msg, ss_msg_len

    mov   ax,ss
    call  w2a    
    puts  wascii, wascii_len

    puts  ds_msg, ds_msg_len

    mov   ax,ds
    call  w2a    
    puts  wascii, wascii_len

    puts  es_msg, es_msg_len

    mov   ax,es
    call  w2a    
    puts  wascii, wascii_len

    puts  fs_msg, fs_msg_len

    mov   ax,fs
    call  w2a    
    puts  wascii, wascii_len

    puts  gs_msg, gs_msg_len

    mov   ax,gs
    call  w2a    
    puts  wascii, wascii_len

    call  _putlf

    puts  baseaddr, baseaddr_len     

    lea   rax,[_start]    ; pode ser "mov rax,_start".
    call  q2a
    puts  qascii, qascii_len

    call  _putlf

    exit0

  ; -----
  ; Converte qword para ascii
  ; Entrada: RAX
  ;
  q2a:
    mov   ecx,16
    lea   rdi,[qascii+17]
  .L1:
    mov   rbx,rax
    and   rbx,0x0f
    mov   dl,[hexvals+rbx]
    mov   [rdi],dl
    shr   rax,4
    dec   rdi
    dec   ecx
    jnz   .L1
    ret

  ; -----
  ; Converte word para ascii
  ; Entrada: AX
  ;
  w2a:
    mov   ecx,4
    lea   rdi,[wascii+5]
    jmp   q2a.L1

  ; Imprime string em stdout.
  ; Entrada: RSI aponta para a string
  ;          EDX tem o tamanho da string.
  _puts:
    mov   eax,1
    mov   edi,STDOUT_FILENO
    syscall
    ret

  ; Imprime salto de linha.
  _putlf:
    lea   rsi,[linefeed]
    mov   edx,1
    jmp   _puts

Ao compilar os dois códigos você pode comparar as imagens binárias:

$ ls -go
total 28
-rwxr-xr-x 1 4560 Nov 19 11:03 getregs
-rwxr-xr-x 1 1064 Nov 19 11:17 getregs2
-rw-r--r-- 1 1970 Nov 19 11:30 getregs2.asm
-rw-r--r-- 1  492 Nov 19 11:03 getregs.c

Ora, temos uma diferença de apenas 3.5 kB na imagem binária dos arquivos. De onde vieram esses 3.5 kB na versão em C? Do código de inicialização e manutenção da libc!

Mas a imagem binária não é a única coisa a ser considerada… É claro que a versão em C gastará mais espaço na memória, justamente por causa da libc… A versão em ASM ocupará exatamente o espaço necessário para conter o código e as “variáveis”… Só que esse espaço adicional, na versão em C, não é significativo. No máximo, algumas páginas (espaços de 4 kB) adicionais. Ainda: a libc prepara o terreno para coisas como alocação dinâmica e possui um conjunto enorme de funções já definidas (printf, por exemplo). Se você for codificar funções no seu código em assembly, rapidamente esse “gap” de 3.5 kB será preenchido e você obterá uma imagem binária maior do que a equivalente em C!

Note que, no código em C, usei uma pequena macro, com um pequeno código em assembly, para obter o conteúdo dos registradores seletores de segmentos… Insisto que este é a melhor maneira de usar códigos escritos em assembly: Dentro de códigos escritos em C!

Escrever códigos completos em assembly é, essencialmente, perda de tempo. O GCC faz um excelente trabalho de otimização, mesmo sendo restringido à convenção de chamada especificada pela POSIX ABI (ou, no caso do Windows, pela especificação Win64).

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s