x86 ASM Programming for Linux - THE.UNIX.WORLD
Pubblicato da mammon_ il 11/1998
Livello base
Introduzione
/* Questo articolo e' stato tratto dall'Assembly Programming Journal numero 1 e tradotto da Little-John */
::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _ |\ \::::::::::. :::\_____\:::::::::::............................................THE.UNIX.WORLD
Iniziamo
Essenzialmente questo articolo e' una scusa per conciliare i miei due interessi favoriti di coding: il sistema operativo Linux e la programmazione in linguaggio assembly. Tutti e due gli argomenti non necessitano (meglio, non dovrebbero) di una introduzione; come l'assembly Win32, assembly per Linux e' eseguito in protected mode 32-bit... comunque ha il netto vantaggio di permetterti di chiamare le funzioni delle librerie standard C come ogni altra funzione delle normali librerie Linux "condivise". Ho cominciato con una breve introduzione sulla compilazione dei programmi in assembly language per Linux; per una migliore leggibilita' potresti bypassarla e andare direttamente alla sezione su "Le Basi".
Compiling e Linking
---------------------
I due assemblers principali per Linux sono Nasm, l'Assembler (gratis) di Netwide, e GAS, l'Assembler (pure gratis) di Gnu, integrato in GCC. Mi concentrero' su Nasm in questo articolo, lasciando GAS per un altro giorno dal momento che usa la sintassi AT&T e cio' richiederebbe una introduzione piu' prolissa.
Nasm dovrebbe essere azionato con l'opzione di formato ELF ("nasm -f elf hello.asm"); l'object che ne deriva e' poi linkato con GCC ("gcc hello.o") per creare il binario ELF finale. Lo script seguente puo' essere usato per compilare moduli ASM; l'ho scritto in modo che sia molto semplice, quindi tutto cio' che fa e' prendere il primo filename passatogli (io consiglio di chiamarlo con una estensione ".asm"), lo compila con nasm, e lo linka con gcc.
#!/bin/sh
# assemble.sh =========================================================
outfile=${1%%.*}
tempfile=asmtemp.o
nasm -o $tempfile -f elf $1
gcc $tempfile -o $outfile
rm $tempfile -f
#EOF ==================================================================
Le Basi
----------
La cosa migliore per partire, naturalmente, e' un esempio, prima di immergerci nei dettagli dell'OS. Ecco qui un programma "hello-world" davvero semplice:
; asmhello.asm ========================================================
global main
extern printf
section .data
msg db "Helloooooo, nurse!",0Dh,0Ah,0
section .text
main:
push dword msg
call printf
pop eax
ret
; EOF =================================================================
Una spiegazione veloce: il "global main" deve essere dichiarato global--e dal momento che stiamo usando il linker GCC, l'entrypoint deve essere chamato "main"--per il loader dell'OS.
La sintassi di chiamata C
--------------------
Linux usa la convenzione di chiamata C -- cio' significa che gli argomenti sono pushati nello stack in ordine inverso (l'ultimo arg per primo), e che il caller deve pulire lo stack. Puoi far cio' o poppando i valori dallo stack:
push dword szText
call puts
pop ecx
o modificando direttamente ESP:
push dword szText
call puts
add esp, 4
I valori restituiti dalla call si trovano in eax o in edx:eax se il valore e' piu' grande di 32-bit. EBP, ESI, EDI, e EBX sono tutti salvati e ripristinati dal caller.
; loop.asm =================================================================
global main
extern printf
section .text
msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0
main:
mov ecx, 0Ah
push dword msg
looper:
call printf
loop looper
pop eax
ret
; EOF ======================================================================
A primo acchito questo sembra molto semplice: dal momento che stai per usare la stringa nelle call 10 printf(), non hai bisogno di ripulire lo stack.
; loop.asm ================================================================
global main
extern printf
section .text
msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0
main:
mov ecx, 0Ah
looper:
push ecx ;salva Count
push dword msg
call printf
pop eax ;pulisce lo stack
pop ecx ;ripristina Count
loop looper
ret
; EOF ======================================================================
Programmazione della Porta I/O
--------------------------------
E per avere un accesso diretto all'hardware? In Linux hai bisogno di un driver kernel-mode per fare ogni cosa che sia davvero ingegnosa... cio' significa che il tuo programma finira' per essere di due parti, una kernel-mode che fornisce le funzionalita' direct-hardware, l'altra user-mode per una interface. La buona notizia e' che puoi ancora accedere alle porta usando i comandi IN/OUT da un programma user-mode.
L'accesso alle porte I/O al tuo programma deve essere concesso da un permesso dell'OS; per far cio', devi compiere una call ioperm(). Questa funzione puo' essere chiamata solo da un utente root, quindi devi o setuid() il programma come root oppure eseguire il programma da root. La ioperm() ha la sintassi seguente:
ioperm( long StartingPort#, long #Ports, BOOL ToggleOn-Off)
dove 'StartingPort#' specifica il numero della prima porta da accedere (0 is port 0h, 40h is port 40h, etc), '#Ports' specifica quante porte accedere (i.e., 'StartingPort# = 30h' e '#Ports = 10' concederebbero l'accesso alle porte 30h-39h), e 'ToggleOn-Off' consente l'accesso se TRUE (1) o lo disabilita se FALSE (0).
; io.asm====================================================================
BITS 32
GLOBAL szHello
GLOBAL main
EXTERN printf
EXTERN ioperm
SECTION .data
szText1 db 'Enabling I/O Port Access',0Ah,0Dh,0
szText2 db 'Disabling I/O Port Acess',0Ah,0Dh,0
szDone db 'Done!',0Ah,0Dh,0
szError db 'Error in ioperm() call!',0Ah,0Dh,0
szEqual db 'Output/Input bytes are equal.',0Ah,0Dh,0
szChange db 'Output/Input bytes changed.',0Ah,0Dh,0
SECTION .text
main:
push dword szText1
call printf
pop ecx
enable_IO:
push word 1 ; enable mode
push dword 04h ; 4 porte
push dword 40h ; inizia dalla porta 40
call ioperm ; Deve essere SUID "root" per questa call!
add ESP, 10 ; pulisci lo stack (metodo 1)
cmp eax, 0 ; controlla i risultati di ioperm()
jne Error
;---------------------------------------Port Programming Part--------------
SetControl:
mov al, 96 ; R/W low byte di Counter2, mode 3
out 43h, al ; porta 43h = control register
WritePort:
mov bl, 0EEh ; valore da inviare allo speaker timer
mov al, bl
out 42h, al ; porta 42h = speaker timer
ReadPort:
in al, 42h
cmp al, bl ; il byte dovrebbe essere cambiato--questo E' un timer :)
jne ByteChanged
BytesEqual:
push dword szEqual
call printf
pop ecx
jmp disable_IO
ByteChanged:
push dword szChange
call printf
pop ecx
;---------------------------------------End Port Programming Part----------
disable_IO:
push dword szText2
call printf
pop ecx
push word 0 ; disable mode
push dword 04h ; 4 porte
push dword 40h ; parte dalla porta 40h
call ioperm
pop ecx ;pulisci lo stack (metodo 2)
pop ecx
pop cx
cmp eax, 0 ; controlla i risultati di ioperm()
jne Error
jmp Exit
Error:
push dword szError
call printf
pop ecx
Exit:
ret
; EOF ======================================================================
Usare gli Interrupts In Linux
-------------------------
Linux e' un ambiente shared-library in protected mode, il che significa che non ci sono i servizi interrupt. Giusto?
Sbagliato. Ho notato una call a INT 80 sul codice di alcuni esempi GAS con il commento "sys_write(ebx, ecx, edx)". Questa funzione e' parte della syscall dell'interfaccia di Linux, e cioe' l'interrupt 80 deve essere un gate ai servizi di syscall. Girovagando nel codice sorgente di Linux (e ignorando gli avvisi di NON USARE MAI l'interface INT 80 siccome i numeri della funzione potrebbere essere cambiati all'improvviso), ho trovato i "system call numbers" --che indicano la funzione da passare a INT 80 per ogni routine di syscall-- nel file UNISTD.H. Ce ne sono 189, quindi non li elenchero' qui... ma se ti accingi a programmare in Linux assembly, fa' un favore a te stesso e stampa questo file.
Quando chiami INT 80h, eax deve contenere il numero della funzione desirata. Tutti i parametri alla routine syscall devono trovarsi nei seguenti registri in questo ordine:
ebx, ecx, edx, esi, edi
quindi il parametro uno si trova in ebx, il parametro 2 in ecx, ecc. Nota non si usa lo stack per passare i valori alla routine syscall. Il risultato della call sara' restituito in eax.
;pid.asm====================================================================
BITS 32
GLOBAL main
EXTERN printf
SECTION .data
szText1 db 'Getting Current Process ID...',0Ah,0Dh,0
szDone db 'Done!',0Ah,0Dh,0
szError db 'Error in int 80!',0Ah,0Dh,0
szOutput db '%d',0Ah,0Dh,0 ;la strana formattazione e' per printf()
SECTION .text
main:
push dword szText1 ;messaggio di apertura
call printf
pop ecx
GetPID:
mov eax, dword 20 ; getpid() syscall
int 80h ; syscall INT
cmp eax, 0 ; non sara' mai PID 0 ! :)
jb Error
push eax ; passa il valore restituito a printf
push dword szOutput ; passa il formato della stringa a printf
call printf
pop ecx ; pulisci lo stack
pop ecx
push dword szDone ; messaggio di chiusura
call printf
pop ecx
jmp Exit
Error:
push dword szError
call printf
pop ecx
Exit:
ret
; EOF =====================================================================
Conclusioni
Ultime considerazioni
-----------------------
Il piu' dei problemi derivera' dall'abituarsi a Nasm stesso. Mentre nasm non e' fornito di una man page, non la installa per default, quindi devi spostarla (cp or mv) da /usr/local/bin/nasm-0.97/nasm.man in /usr/local/man/man1/nasm.man
La formattazione e' un po' incasinata, ma e' facilmente risistemata usando le direttive nroff. Non ti da' ancora tutta la documentazione di Nasm, comunque; per questo, copia nasmdoc.txt da /usr/local/bin/nasm-0.97/doc/nasmdoc.txt in /usr/local/man/man1/nasmdoc.man
Ora puoi chiamare man page di nasm con 'man nasm' e la documentazione di nasm con 'man nasmdoc'.
Per ulteriori informazioni, controlla i seguenti: