Stato iniziale dei registri del programma e stack su Linux ARM
Domanda
Attualmente sto giocando con ARM assembly su Linux come esercizio di apprendimento. Sto usando un assembly "bare", ovvero senza libcrt o libgcc. Qualcuno può indicarmi informazioni su quale stato faranno il puntatore dello stack e altri registri all'inizio del programma prima che venga chiamata la prima istruzione? Ovviamente pc / r15 punti a _start, e il resto sembra essere inizializzato su 0, con due eccezioni; sp / r13 indica un indirizzo molto al di fuori del mio programma e r1 indica un indirizzo leggermente più alto.
Quindi, per alcune solide domande:
- Qual è il valore in r1?
- Il valore in sp è uno stack legittimo allocato dal kernel?
- In caso contrario, qual è il metodo preferito per allocare uno stack; usando brk o allocare una sezione .bss statica?
Eventuali puntatori sarebbero apprezzati.
Soluzione
Ecco cosa uso per avviare un programma Linux / ARM con il mio compilatore:
/** The initial entry point.
*/
asm(
" .text\n"
" .globl _start\n"
" .align 2\n"
"_start:\n"
" sub lr, lr, lr\n" // Clear the link register.
" ldr r0, [sp]\n" // Get argc...
" add r1, sp, #4\n" // ... and argv ...
" add r2, r1, r0, LSL #2\n" // ... and compute environ.
" bl _estart\n" // Let's go!
" b .\n" // Never gets here.
" .size _start, .-_start\n"
);
Come puoi vedere, ho appena preso le cose argc, argv e environment dallo stack su [sp].
Un piccolo chiarimento: il puntatore dello stack punta a un'area valida nella memoria del processo. r0, r1, r2 e r3 sono i primi tre parametri della funzione chiamata. Li popolo con argc, argv e ambi, rispettivamente.
Altri suggerimenti
Dato che si tratta di Linux, puoi vedere come viene implementato dal kernel.
I registri sembrano essere impostati dalla chiamata su start_thread
alla fine di load_elf_binary
(se stai usando un moderno sistema Linux, userà quasi sempre il formato ELF). Per ARM, i registri sembrano essere impostati come segue:
r0 = first word in the stack
r1 = second word in the stack
r2 = third word in the stack
sp = address of the stack
pc = binary entry point
cpsr = endianess, thumb mode, and address limit set as needed
Chiaramente hai uno stack valido. Penso che i valori di r0
- r2
siano spazzatura e dovresti invece leggere tutto dallo stack (vedrai perché lo penso più tardi). Ora diamo un'occhiata a ciò che è nello stack. Quello che leggerai dallo stack è compilato da create_elf_tables
.
Una cosa interessante da notare qui è che questa funzione è indipendente dall'architettura, quindi le stesse cose (principalmente) verranno messe in pila su ogni architettura Linux basata su ELF. Quanto segue è nello stack, nell'ordine in cui lo leggeresti:
- Il numero di parametri (questo è
argc
inmain ()
). - Un puntatore a una stringa C per ciascun parametro, seguito da uno zero (questo è il contenuto di
argv
inmain ()
;argv
indica il primo di questi puntatori). - Un puntatore a una stringa C per ogni variabile d'ambiente, seguito da uno zero (questo è il contenuto del terzo parametro
envp
raramente visto dimain ()
;envp
punta al primo di questi puntatori). - Il "vettore ausiliario", che è una sequenza di coppie (un tipo seguito da un valore), terminata da una coppia con uno zero (
AT_NULL
) nel primo elemento. Questo vettore ausiliario ha alcune informazioni interessanti e utili, che puoi vedere (se stai usando glibc) eseguendo qualsiasi programma collegato dinamicamente con la variabile d'ambienteLD_SHOW_AUXV
impostata su1
(ad esempioLD_SHOW_AUXV = 1 / bin / true
). Questo è anche il luogo in cui le cose possono variare leggermente a seconda dell'architettura.
Poiché questa struttura è la stessa per ogni architettura, è possibile cercare ad esempio il disegno a pagina 54 del SYSV 386 ABI per avere un'idea migliore di come le cose si incastrano (si noti, tuttavia, che le costanti del tipo di vettore ausiliario su quel documento sono diverse da quelle utilizzate da Linux, quindi si dovrebbe guardare Linux intestazioni per loro).
Ora puoi vedere perché i contenuti di r0
- r2
sono spazzatura. La prima parola nello stack è argc
, la seconda è un puntatore al nome del programma ( argv [0]
), e la terza probabilmente era zero per te perché hai chiamato il programma senza argomenti (sarebbe argv [1]
). Immagino che siano impostati in questo modo per il vecchio formato binario a.out
, che come puoi vedere in create_aout_tables
inserisce argc
, argv
e envp
nello stack (quindi finirebbero in r0
- r2
nell'ordine previsto per una chiamata a main ()
).
Infine, perché r0
era zero per te anziché uno ( argc
dovrebbe essere uno se hai chiamato il programma senza argomenti)? Immagino che qualcosa di profondo nel macchinario di syscall lo abbia sovrascritto con il valore di ritorno della chiamata di sistema (che sarebbe zero poiché l'esecutore è riuscito). Puoi vedere in kernel_execve
(che non utilizza il macchinario syscall, poiché è ciò che il kernel chiama quando vuole eseguire dalla modalità kernel) che sovrascrive deliberatamente r0
con il valore restituito do_execve
.
Ecco il uClibc crt . Sembra suggerire che tutti i registri non siano definiti tranne r0
(che contiene un puntatore a funzione da registrare con atexit ()
) e sp
che contiene un indirizzo stack valido.
Quindi, il valore che vedi in r1
probabilmente non è qualcosa su cui puoi contare.
Alcuni dati vengono inseriti nello stack per te.
Non ho mai usato ARM Linux ma ti suggerisco di guardare il sorgente per libcrt e vedere cosa fanno, o usare gdb per entrare in un eseguibile esistente. Non dovresti aver bisogno del codice sorgente, basta scorrere il codice assembly.
Tutto ciò che devi scoprire dovrebbe accadere nel primo codice eseguito da qualsiasi eseguibile binario.
Spero che questo aiuti.
Tony