"Gazeta do Linux...fazendo o Linux um pouco mais divertido!"


Uma Introdução a Programação Assembly em Unix

Por Konstantin Boldyshev

Tradução para Português por José Elias Gomes de Lima

23 de novembro de 2002, para a Gazeta do Linux


Este artigo tem a pretensão de ser um tutorial, mostrando como escrever um programa simples que funciona em diversos sistemas UNIX na plataforma IA32 (i386). O material incluído pode ou não ser aplicável a outros hardware e/ou SO. O artigo explica a disposição do programa, as convenções de chamada ao sistema, e o processo de configuração. Acompanha o Linux Assembly HOWTO, que também pode ser de seu interesse, embora seja mais especifico para Linux.

v0.3, April 09, 2000


1. Introdução

1.1 Direitos Legais

Copyright © 1999-2000 Konstantin Boldyshev. Permitida a reprodução, distribuição e/ou modificação deste artigo sob os termos da GNU  Free Documentation License, Versão 1.1 ou alguma versão antiga publicada pela Free Software Foundation.

1.2 Obtendo este artigo

A última versão deste artigo está disponível em  http://linuxassembly.org/intro.html (inglês). Se você estiver lendo uma cópia um pouco mais antiga, por favor, verifique a URL acima para ver se há uma nova versão.

1.3 Ferramentas que você precisará

Você necessitará de várias ferramentas para executar os programas incluídos nesse tutorial.

Primeiramente, você precisará do assembler (compilador). Como regra geral, as distribuições modernas de UNIX incluem o gas (Compilador GNU), mas todos os exemplos especificados aqui podem usar um outro compilador -- nasm (Compilador da Netwide). Você pode baixa-lo da página do  nasm, ele vem com código fonte. Compile-o, ou tente encontrar o binário pré-compilado para seu SO; note que diversas distribuições (exceto alguns Linux) já tem o nasm, verifique antes.

Segundo, você vai precisar do linkador -- ld, pois o nasm só produz o código objeto. Todas as distribuições adotam o ld.

Se você pretende ir mais fundo, instale também arquivos para seu OS, e se possível, o código fonte do Kernel.

Agora você esta pronto para começar, seja bem vindo!..


2. Hello, world!

Agora nós escreveremos nosso programa, o clássico "Hello, world" (hello.asm). Você pode baixar o código fonte e binários aqui. Mas antes deixe-me explicar os princípios básicos..

2.1 System call

A não ser que o programa seja uma implementação de algum algoritmo matemático em assembly, este tratará tais coisas como entrada, produzirá uma saída e encerrará o programa. Vem então a necessidade de chamar algum serviço ao SO. De fato, programar em linguagem assembly é completamente o mesmo em Sistemas Operacionais diferentes, a menos que os serviços do OS sejam específicos.

Há dois meios comuns de executar uma chamada ao sistema no SO UNIX: utilizar as bibliotecas C (libc), ou diretamente.

Usar ou não libc em programação assembly é mais uma questão de gosto/opção do que algo prático. Os pacotes Libc são feitas para proteger o sistema de possíveis mudanças de convenção nas chamadas ao sistema, e prover uma interface compatível para POSIX, se o kernel faltar a alguma chamada. No entanto, geralmente o kernel UNIX é mais ou menos compatível com o POSIX, isto quer dizer que a maioria da sintaxe da libc chamadas ao sistema são exatamente iguais a sintaxe de chamadas ao sistema do kernel (e vice-versa). Mas, a principal desvantagem de desperdiçar a libc é que são negligenciadas várias funções que não estão contidas na syscall, como printf(), malloc() e semelhantes..

Este tutorial mostra como usar as chamadas diretas ao kernel, visto que este é o modo mais rápido para chamar os serviços do kernel; nosso código não é linkado a uma biblioteca, este se comunica diretamente com o kernel

As coisas que diferem em diferentes kernels UNIX são as chamadas ao sistema e convenções de chamadas ao sistema (porém, eles se esforçam para torna compatível o POSIX, há muita em comum entre eles).

Nota (do autor) para os programadores DOS: bem, o que é uma chamada ao sistema?? Melhor explicar desta forma: se você sempre escreve um programa assembly para DOS (como a maioria dos programadores assemblyIA32), você deve lembrar dos serviços int 0x21, int 0x25, int 0x26 etc. Isto é o que pode ser chamado de chamada ao sistema. Porém, a atual implementação é absolutamente diferente, e isto não significa necessariamente que as chamadas ao sistema sejam feitas via alguma interrupção. Também, é bastante comum aos programadores DOS misturarem os serviços do SO com os serviços de BIOS, como int 0x10 ou int 0x16, e ficam muito surpesos quando elas não executam em UNIX, pois elas não são serviços do OS).

2.2 Layout do programa

Como regra, modernamente os UNIXes IA32 são 32bit (*grin*), executam em modo protegido, tem modelo de memória baixa, e usam formato ELF para binários.

O programa pode ser dividido em secções (ou segmentos): .text para seus código(somente leitura, .data para seus dados (leitura-escrita), .bss para dados não inicializáveis (leitura-escrita); atualmente pode haver outros, mas raramente eles precisam ser usados e não nos interessam no momento. O programa tem que ter pelo menos a seção .text.

Ok, agora entraremos em detalhes específicos do OS.

2.3 Linux

As chamadas ao sistema em Linux são feitas através da int 0x80. (atualmente há um patch do kernel que permitindo que as chamadas ao sistema sejam feitas através da instrução syscall (sysenter) em CPUs mais novas, mas estas coisas ainda são experimentais).

 Linux difere das habituais convenções de chamadas UNIX, e retrata as convenções fastcall de chamadas ao sistema (semelhante em DOS). O número de funções do sistema são passadas em eax, e os argumentos são passados para o registro, não para a pilha. Pode haver até cinco argumentos ebx, ecx, edx, esi e edi, respectivamente. Se houver mais de cinco argumentos, eles simplesmente serão passados como primeiro argumento. O resultado é retornado em eax e a pilha não é totalmente preenchida.

Os números de funções de chamadas ao sistema estão em sys/syscall.h, mas atualmente estas estão em asm/unistd.h, alguma documentação está na 2a secção do manual (f.e. procure informações em chamada do sistema write, issue man 2 write).

Há várias tentativas de se fazer uma documentação de atualização para chamadas ao sistema em Linux, examine URLs nas referências.

Assim, nosso programa em Linux ficará da seguinte forma:


section .text
    global _start                       ;must be declared for linker (ld)

msg     db      'Hello, world!',0xa     ;our dear string
len     equ     $ - msg                 ;length of our dear string

_start:                 ;we tell linker where is entry point

        mov     edx,len ;message length
        mov     ecx,msg ;message to write
        mov     ebx,1   ;file descriptor (stdout)
        mov     eax,4   ;system call number (sys_write)
        int     0x80    ;call kernel

        mov     eax,1   ;system call number (sys_exit)
        int     0x80    ;call kernel

Como vocês verão mais adiante, em Linux as convenções syscall são muitas vezes mais compactas.

Refêrencias para o código fonte do Kernel:

2.4 FreeBSD

FreeBSD tem a "habitual" convenção de chamada, quando o número syscall está em eax, e os parâmetros estão na pilha (o primeiro argumento é empurrado para o final). As chamadas ao sistema serão executadas pela chamada de função para a função que contém int 0x80 e ret, não só int 0x80 (o endereço de RETORNO deve estar na pilha antes de int 0x80 ser emitido!). Deve-se limpar a pilha depois da chamada. O resultado é devolvido como sempre em eax.

Também há um modo de usar a chamada 7:0 em vez de int 0x80. O resultado final é o mesmo. Não contando o acréscimo do tamanho do programa, desde que, você também precisará enviar eax antes, e estas duas instruções ocupam mais bytes.

O número das chamadas de função estão em sys/syscall.h, a documentação está na 2a secção do manual.

Ok, eu acho que o código explicará isto melhor:

Nota: O código incluído pode ser executado em outro *BSD, eu acho.


section .text
    global _start                       ;must be declared for linker (ld)

msg     db      "Hello, world!",0xa     ;our dear string
len     equ     $ - msg                 ;length of our dear string

_syscall:               
        int     0x80            ;system call
        ret

_start:                         ;tell linker entry point

        push    dword len       ;message length
        push    dword msg       ;message to write
        push    dword 1         ;file descriptor (stdout)
        mov     eax,0x4         ;system call number (sys_write)
        call    _syscall        ;call kernel

                                ;actually there's an alternate
                                ;way to call kernel:
                                ;push   eax
                                ;call   7:0

        add     esp,12          ;clean stack (3 arguments * 4)

        push    dword 0         ;exit code
        mov     eax,0x1         ;system call number (sys_exit)
        call    _syscall        ;call kernel

                                ;we do not return from sys_exit,
                                ;there's no need to clean stack

Referências no código do Kernel:

2.5 BeOS

O Kernel BeOS normal convenção de chamada, também. A diferença do exemplo para FreeBSD é que você chama int 0x25.

Para achar informações sobre números de funções de chamada do sistema e outros detalhes interessantes, examine asmutils, especialmente o arquivo os_beos.inc.

Nota: para fazer o nasm compilar corretamente no BeOS você deve inserir #include nasm.h em float.h, e #include <stdio.h> em nasm.h.


section .text
    global _start                       ;must be declared for linker (ld)

msg     db      "Hello, world!",0xa     ;our dear string
len     equ     $ - msg                 ;length of our dear string

_syscall:                       ;system call
        int     0x25
        ret

_start:                         ;tell linker entry point

        push    dword len       ;message length
        push    dword msg       ;message to write
        push    dword 1         ;file descriptor (stdout)
        mov     eax,0x3         ;system call number (sys_write)
        call    _syscall        ;call kernel
        add     esp,12          ;clean stack (3 * 4)

        push    dword 0         ;exit code
        mov     eax,0x3f        ;system call number (sys_exit)
        call    _syscall        ;call kernel
                                ;no need to clean stack

2.6 Construindo o binário

A construção do arquivo binário é feito geralmente em dois passos: compilando e linkando. Para fazer nosso hello.asm, nós temos que fazer o seguinte:


$ nasm -f elf hello.asm         # this will produce hello.o object file
$ ld -s -o hello hello.o        # this will produce hello executable

É isto. Simples. Agora você pode rodar seu programa fazendo ./hello, este deve trabalhar. Veja o tamanho do binário -- surpreso?


3. Referências

Eu espero que você tenha desfrutado da viagem. Se você se interessa por programação assembly em Unix, eu recomendo que você visite Linux Assembly para mais informações e baixe o pacote asmutils, contém muitos exemplos de código. Para compreender melhor a programação assembly para Linux/UNIX recorra ao Linux Assembly HOWTO.

Obrigado por seu interesse!


Copyright © 2000, Konstantin Boldyshev
Publicado no Número 53 da Gazeta do Linux, Maio de 2000