Assembly no Linux/Exemplos de uso de syscalls

Outra semelhança com os procedimentos usados para o DOS. Temos o compilador (nasm) e o linker (Id), portanto primeiro será gerado um arquivo objeto, com extensão. O e, depois, com o uso do linker Id, será transformado em um executável. Com uma pesquisa no manual do Id (man Id) você pode aprender um pouco mais sobre suas opções e recursos. Agora as opções utilizadas são somente as necessárias para gerarmos um arquivo executável.

Hello world

editar

Qualquer introdução a uma linguagem, compilador ou qualquer ferramenta ligada à programação não é completa sem o exemplo do hello world. Neste exemplo seguimos o seguinte raciocínio.

Temos para cada processo no Linux, três file descriptors que são criados e definidos automaticamente, que são: stdin, stdout, e stderr. Um file descriptor é um código que indica um arquivo. No nosso caso os arquivos são, respectivamente: entrada padrão do terminal. Em um programa em C podemos fazer o seguinte: write (1, “oi”, 2); que teremos como saída na tela a palavra “oi”.

Isto se explica por que escrevemos (write) no arquivo indicado pelo fd (file descriptor) número 1, uma string de 2 caracteres.

Seguindo este raciocínio, vemos que não tem uma syscall chamada print, como a maioria pode esperar, mas sim, a SYS_write. Portanto, traduzindo para assembler a linha acima, teremos impressa a string que queremos na tela.

Esta syscall pede três parâmetros a seguir: ebx com o numero do file descriptor (no nosso caso 1, stdout), ecx um ponteiro (endereço da memória) para a string e edx o tamanho da mensagem. O registro eax deve estar carregado com o valor 4, que na tabela corresponde a SYS_exit, para sair corretamente do programa, avisando o kernel para limpar os recursos usados. O registro eax é carregado com 1 (código de SYS_exit), e int 0x80 é chamada novamente, finalizando assim o programa.

; Exemplo 1
; hello world em assembly
; usando syscalls [ int 0x80 ]
;
;
; compilar com nasm –f elf hello.s
; linkar com ld –s –o hello hello.o
;
 
Section .text         align=0
Global _start                       ; inicio Do programa para o mundo é _start
msg        db           Hello World , 0x0a ;mensagem + LF, para pular a linha
len        equ          $ - msg   ; tamanho da mensagem
 
_start:                                    ; entrypoint Ponto de execução inicial
 
 
                mov eax,4                  ; número da função [SYS_write] write
                mov ebx,1                  ; número Do fd [ file descriptor ] no caso, Stdout
                mov ecx,msg                ; ponteiro da mensagem
                mov dx,len                 ; tamanho da mensagem
                ; write [1, msg, len];
                int 0x80  ; syscall
 
                mov eax,1                  ; número da função [SYS_exit] exit
                mov ebx,0                  ; parâmetro do sys_exit (0=OK, 1=Erro)
                int 0x80                   ; chamada do kernel
                ; Fim do código

Lê parâmetros da linha de comando e os imprime

editar

Este segundo exemplo, um pouco mais elaborado, lê os parâmetros passados pela linha de comando, limpa a tela e imprime-os. Novamente, a criatividade de ser usada para suprir nossas necessidades.

Quando executamos um programa, do tipo ./programa parm1parm2, após as inicializações do kernel, quando o controle é entregue à primeira função do programa, normalmente chamada start ou main, temos a pilha da maquina (stack) e os registros com alguns valores carregados, tais como parâmetros de linha de comando, variáveis de ambiente, em suma, o que receberíamos na função main de um programa em C:

Int main (int argc, char**argv, char**envp);,

Onde argc, é o numero de argumentos passados pela linha de comando, argv uma matriz de ponteiros para os argumentos, terminada em NULL e envp uma matriz de ponteiros para as variáveis de ambiente.

Todos estes dados estão no stack, na seguinte ordem: argc, todos os ponteiros argv, seguidos por um NULL, e todos os ponteiros envp seguidos por um NULL.

Portanto, para recuperarmos estes dados, simplesmente vamos retirando (instrução pop) os valores do stack, e tratando-os.

Reparem que no início do programa, algumas strings são definidas, tais como LF (line feed), para mudarmos de linha e uma string ANSI, com o código que limpa a tela. Não temos uma syscall que faz isso, nem faria muito sentido, visto que o terminal é apenas uma das interfaces que podemos encontrar em um ambiente Unix. N maioria dos terminais, que é comparável com ANSI, este código surte efeito. No DOS, dependendo da versão, deveríamos usar o driver ANSI.SYS no confing.sys, para que tais códigos fossem interpretados. É com uma solução semelhante a esta que fizemos o HOME, ou seja, a função que faz o cursor se posicionar nas coordenadas 0,0, canto superior esquerdo da tela.

Note que após clrs e home, já calculamos os respectivos tamanhos, pois as imprimiremos com a técnica usada no Exemplo 1, SYS_write e stdout.

Assim, com a tela limpa e o cursor em (0,0), o processamento é iniciado com a checagem do número de parâmetros, se não forem passados nenhum parâmetro, o programa deve sair imediatamente. Caso contrario, deve haver um loop que enquanto eles não terminem(testando se é NULL), é neste loop que será calculado o tamanho de cada um e impresso na tela, usando a técnica do exemplo anterior. Ao final, SYS_exit é chamada.

Verifique o uso intensivo de loops e labels, que apesar de parecerem complexos, guardam uma lógica bem simples, como usar o registro que deve passar o parâmetro de tamanho da string para SYS_write, como acumulador no loop que calcula este dado (strlen). Assim, ao final do loop, o parâmetro já esta correto e economiza mais trabalho.

Este exemplo é bem mais elaborado, mas serve para ilustrar as possibilidades existentes.

; Exemplo 2
;
; limpa a tela
; passagem de parâmetros por linha de Comando
; imprime o que foi passado
; muda de linha
;
; os parâmetros são pegos do stack
;
;
; compilar com : nasm –f elf prog.s
; linkar com            : ld –s –o prog prog.0
;
;
Section  .text        align=0
Global                   _start
 
; crlf – representa os códigos para mudar de linha e retornar ao início
line                        de 0x0a
len                         que $ - line
; limpa a tela usando códigos ANSI
 
clrs                         db 0x16, [2J
clslen                                    equ $ - clrs
 
; vai para 0,0, ocm um bkspace
; usando ANSI
 
home                     db 0x16, [0H , 0x16,
  [0C , 0x08
holen                     equ $ - home
 
; início
 
 
_start:
 
 
; limpa a tela
 
                mov eax, 4             ; write
                mov ebx, 1             ; stdout [fd]
                mov ecx, clrs        
                mov edx, clslen
                int 0x80                  ; syscall
 
 
; início do processamento
 
 
                Pop eax  ; testa argc,para verificar se existem parametros
                Dec eax  ;
                Jz exit                     ; se não existem, sai do programa
                pop ecx
                                               ; note q argv [0] é sempre o nome do programa.
                                               ; como não queremos que ele apareça,
                                               ; faremos o stackpointer andar [o índice do stack] até argv [1]
 
 
;  Loop executado até que todos os argumentos sejam impressos
mainloop:                            ; lavel do loop principal
 
                pop ecx; pega parametro
                ore cx, ecx             ; testa se é zero [NULL]
                Jz exit     ; ecx        ; goto [ ble ] Exit
 
                mob rdi, ecx
                xor edx, edx
 
.strlen:
encontra o tamanho da string deste argumento [argv[]]
                ince dx                   ; esta é uma versão simples de strlen, que conta cada caractere
                lodsb                     ; de uma string até o seu final [\0]
                or AL, AL
                jnz .strlen
                dec edx
 
                mov eax, 4             ; write
                mov ebx, 1             ; stdout
                int 0x80                  ; syscall
; pula linha após escrever
 
                mov eax, 4
                mov ebx, 1
                mov cx,  line
                move dx, len
                int 0x80
 
                jmp mainloop      ; continua
 
exit:
                move ax, 1             ; final do programa
                int 0x80                  ; syscall
;
;
; Fim do código

Usando bibliotecas com código Assembly

editar

Além de usar diretamente as syscalls, podemos também nos aproveitar das muitas bibliotecas (libraries) presentes em nosso sistema, para aliar a velocidade do Assembly com a praticidade de rotinas prontas. Basicamente, isso significa passar os argumentos no stack e chamar a rotina desejada usando call. Na hora de linkar, devem ser indicadas as bibliotecas corretas, ou no caso da libc, o Id automaticamente pode linkar da forma correta.

O procedimento para compilação muda um pouco, porque usaremos o gcc para nos ajudar a linkar e resolver as pendências de bibliotecas em vez do Id, que exigiria mais parâmetros em linhas de comando. Na realidade o gcc vai usar o Id, mas nos poupará trabalho. A única mudança neste caso, é trocar de_start para main, para que o gcc entenda que este é o ponto de entrada.

Usando libc

editar

Neste exemplo, simplesmente utilizamos uma chamada à função printf, da libc, para imprimir nossa string, em vez de imprimi-la caractere a caractere usando SYS_write. Quando chamamos uma função de uma biblioteca, seus parâmetros devem estar em ordem, na pilha.

; Exemplo 3
; Hello World usando libc [prinft]
; compilar com nasm –lelf hello-world.s
; linkar com gcc –s 0o hello-word hello-word.o
;
 
extern printf                         ; declara printf como um símbolo externo
global main                         ; avisa q main é um ponto de entrada visível
                               ; ao mundo externo do programa.
 
section .text
main:
                               puxha                                    ; Salva todos os registros
push dword messsage                      ; Coloca o ponteiro da mensagem no stack
                call printf                                                             ;
                add esp, 4                                            ;
                popa                                                     ; restaura seus valores
                ret                                                          ; retorna
 
message: db “hello, world!”, 10, 0
 
;
; Fim do código
;