Comprendere il codice assembly generato da un semplice programma C

Comprendere il codice assembly generato da un semplice programma C


Sto cercando di capire il codice a livello di assembly per un semplice programma C ispezionandolo con il disassembler di gdb.


Di seguito è riportato il codice C:


#include <stdio.h>
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}

Di seguito è riportato il codice di smontaggio per entrambi main e function


gdb) disass main
Dump of assembler code for function main:
0x08048428 <main+0>: push %ebp
0x08048429 <main+1>: mov %esp,%ebp
0x0804842b <main+3>: and $0xfffffff0,%esp
0x0804842e <main+6>: sub $0x10,%esp
0x08048431 <main+9>: movl $0x3,0x8(%esp)
0x08048439 <main+17>: movl $0x2,0x4(%esp)
0x08048441 <main+25>: movl $0x1,(%esp)
0x08048448 <main+32>: call 0x8048404 <function>
0x0804844d <main+37>: leave
0x0804844e <main+38>: ret
End of assembler dump.
(gdb) disass function
Dump of assembler code for function function:
0x08048404 <function+0>: push %ebp
0x08048405 <function+1>: mov %esp,%ebp
0x08048407 <function+3>: sub $0x28,%esp
0x0804840a <function+6>: mov %gs:0x14,%eax
0x08048410 <function+12>: mov %eax,-0xc(%ebp)
0x08048413 <function+15>: xor %eax,%eax
0x08048415 <function+17>: mov -0xc(%ebp),%eax
0x08048418 <function+20>: xor %gs:0x14,%eax
0x0804841f <function+27>: je 0x8048426 <function+34>
0x08048421 <function+29>: call 0x8048340 <[email protected]>
0x08048426 <function+34>: leave
0x08048427 <function+35>: ret
End of assembler dump.

Sto cercando risposte per le seguenti cose:



  1. come funziona l'indirizzamento, intendo (principale+0), (principale+1), (principale+3)

  2. In sostanza, perché viene utilizzato $0xfffffff0,%esp

  3. Nella funzione, perché viene utilizzato %gs:0x14,%eax , %eax,-0xc(%ebp).

  4. Se qualcuno può spiegare , passo dopo passo, sarà molto apprezzato.


Risposte:


Il motivo degli indirizzi "strani" come main+0 , main+1 , main+3 , main+6 e così via, perché ogni istruzione occupa un numero variabile di byte. Ad esempio:


main+0: push %ebp

è un'istruzione di un byte, quindi l'istruzione successiva è a main+1 . D'altra parte,


main+3: and $0xfffffff0,%esp

è un'istruzione a tre byte, quindi l'istruzione successiva è a main+6 .


E, visto che nei commenti chiedi perché movl sembra prendere un numero variabile di byte, la spiegazione è la seguente.


La lunghezza delle istruzioni non dipende solo dal opcode (come movl ) ma anche le modalità di indirizzamento per gli operandi anche (le cose su cui sta operando l'opcode). Non ho verificato specificamente il tuo codice ma sospetto che


movl $0x1,(%esp)

l'istruzione è probabilmente più breve perché non è coinvolto alcun offset:utilizza solo esp come indirizzo. Mentre qualcosa come:


movl $0x2,0x4(%esp)

richiede tutto ciò che movl $0x1,(%esp) fa, più un byte extra per l'offset 0x4 .


In effetti, ecco una sessione di debug che mostra cosa intendo:


Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.
c:\pax> debug
-a
0B52:0100 mov word ptr [di],7
0B52:0104 mov word ptr [di+2],8
0B52:0109 mov word ptr [di+0],7
0B52:010E
-u100,10d
0B52:0100 C7050700 MOV WORD PTR [DI],0007
0B52:0104 C745020800 MOV WORD PTR [DI+02],0008
0B52:0109 C745000700 MOV WORD PTR [DI+00],0007
-q
c:\pax> _

Puoi vedere che la seconda istruzione con un offset è effettivamente diversa dalla prima senza di essa. È un byte più lungo (5 byte invece di 4, per mantenere l'offset) e in realtà ha una codifica diversa c745 invece di c705 .


Puoi anche vedere che puoi codificare la prima e la terza istruzione in due modi diversi, ma sostanzialmente fanno la stessa cosa.



Il and $0xfffffff0,%esp l'istruzione è un modo per forzare esp trovarsi su un confine specifico. Viene utilizzato per garantire il corretto allineamento delle variabili. Molti accessi alla memoria sui processori moderni saranno più efficienti se seguono le regole di allineamento (come un valore di 4 byte che deve essere allineato a un limite di 4 byte). Alcuni processori moderni solleveranno persino un errore se non segui queste regole.


Dopo questa istruzione, hai la garanzia che esp è sia minore che uguale al suo valore precedente e allineato a un limite di 16 byte.



Il gs: prefisso significa semplicemente usare il gs registro del segmento per accedere alla memoria anziché al valore predefinito.


L'istruzione mov %eax,-0xc(%ebp) significa prendere il contenuto del ebp registra, sottrai 12 (0xc ) e quindi inserisci il valore di eax in quella posizione di memoria.



Riguarda la spiegazione del codice. Il tuo function la funzione è fondamentalmente un grande no-op. L'assieme generato è limitato alla configurazione e allo smontaggio dello stack frame, insieme ad alcuni controlli di danneggiamento dello stack frame che utilizzano il summenzionato %gs:14 posizione di memoria.


Carica il valore da quella posizione (probabilmente qualcosa come 0xdeadbeef ) nello stack frame, fa il suo lavoro, quindi controlla lo stack per assicurarsi che non sia stato danneggiato.


Il suo lavoro, in questo caso, non è niente. Quindi tutto ciò che vedi è la roba di amministrazione delle funzioni.


La configurazione dello stack avviene tra function+0 e function+12 . Tutto dopo è l'impostazione del codice di ritorno in eax e abbattendo lo stack frame, incluso il controllo della corruzione.


Allo stesso modo, main consistono nella configurazione dello stack frame, spingendo i parametri per function , chiamando function , abbattendo lo stack frame ed uscendo.


I commenti sono stati inseriti nel codice sottostante:


0x08048428 <main+0>:    push   %ebp                 ; save previous value.
0x08048429 <main+1>: mov %esp,%ebp ; create new stack frame.
0x0804842b <main+3>: and $0xfffffff0,%esp ; align to boundary.
0x0804842e <main+6>: sub $0x10,%esp ; make space on stack.
0x08048431 <main+9>: movl $0x3,0x8(%esp) ; push values for function.
0x08048439 <main+17>: movl $0x2,0x4(%esp)
0x08048441 <main+25>: movl $0x1,(%esp)
0x08048448 <main+32>: call 0x8048404 <function> ; and call it.
0x0804844d <main+37>: leave ; tear down frame.
0x0804844e <main+38>: ret ; and exit.
0x08048404 <func+0>: push %ebp ; save previous value.
0x08048405 <func+1>: mov %esp,%ebp ; create new stack frame.
0x08048407 <func+3>: sub $0x28,%esp ; make space on stack.
0x0804840a <func+6>: mov %gs:0x14,%eax ; get sentinel value.
0x08048410 <func+12>: mov %eax,-0xc(%ebp) ; put on stack.
0x08048413 <func+15>: xor %eax,%eax ; set return code 0.
0x08048415 <func+17>: mov -0xc(%ebp),%eax ; get sentinel from stack.
0x08048418 <func+20>: xor %gs:0x14,%eax ; compare with actual.
0x0804841f <func+27>: je <func+34> ; jump if okay.
0x08048421 <func+29>: call <_stk_chk_fl> ; otherwise corrupted stack.
0x08048426 <func+34>: leave ; tear down frame.
0x08048427 <func+35>: ret ; and exit.


Penso che il motivo del %gs:0x14 può essere evidente dall'alto ma, per ogni evenienza, elaboro qui.


Usa questo valore (una sentinella) per inserire lo stack frame corrente in modo che, se qualcosa nella funzione dovesse fare qualcosa di stupido come scrivere 1024 byte su un array di 20 byte creato nello stack o, nel tuo caso:


char buffer1[5];
strcpy (buffer1, "Hello there, my name is Pax.");

quindi la sentinella verrà sovrascritta e il controllo alla fine della funzione lo rileverà, chiamando la funzione di guasto per avvisarti, e quindi probabilmente interrompendo per evitare altri problemi.


Se ha posizionato 0xdeadbeef nello stack e questo è stato cambiato in qualcos'altro, quindi un xor con 0xdeadbeef produrrebbe un valore diverso da zero che viene rilevato nel codice con il je istruzione.


Il bit rilevante è parafrasato qui:


          mov    %gs:0x14,%eax     ; get sentinel value.
mov %eax,-0xc(%ebp) ; put on stack.
;; Weave your function
;; magic here.
mov -0xc(%ebp),%eax ; get sentinel back from stack.
xor %gs:0x14,%eax ; compare with original value.
je stack_ok ; zero/equal means no corruption.
call stack_bad ; otherwise corrupted stack.
stack_ok: leave ; tear down frame.