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
editarQualquer 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
editarEste 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
editarAlé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
editarNeste 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 ;