Comprender el código ensamblador generado por un programa C simple

Comprender el código ensamblador generado por un programa C simple


Estoy tratando de entender el código de nivel de ensamblaje para un programa C simple al inspeccionarlo con el desensamblador de gdb.


El siguiente es el código C:


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

El siguiente es el código de desmontaje para ambos main y 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.

Estoy buscando respuestas para las siguientes cosas:



  1. cómo funciona el direccionamiento, quiero decir (principal+0), (principal+1), (principal+3)

  2. En general, ¿por qué se usa $0xfffffff0,%esp?

  3. En la función, ¿por qué se usa %gs:0x14,%eax, %eax,-0xc(%ebp)?

  4. Si alguien puede explicar, paso a paso, será muy apreciado.


Respuestas:


El motivo de las direcciones "extrañas" como main+0 , main+1 , main+3 , main+6 y así sucesivamente, se debe a que cada instrucción ocupa un número variable de bytes. Por ejemplo:


main+0: push %ebp

es una instrucción de un byte, por lo que la siguiente instrucción está en main+1 . Por otro lado,


main+3: and $0xfffffff0,%esp

es una instrucción de tres bytes, por lo que la siguiente instrucción está en main+6 .


Y, ya que preguntas en los comentarios por qué movl parece tomar un número variable de bytes, la explicación es la siguiente.


La longitud de la instrucción depende no solo del opcode (como movl ) sino también los modos de direccionamiento para los operandos también (las cosas en las que opera el código de operación). No he buscado específicamente su código, pero sospecho que


movl $0x1,(%esp)

la instrucción es probablemente más corta porque no hay compensación involucrada, solo usa esp como la dirección. Mientras que algo como:


movl $0x2,0x4(%esp)

requiere todo lo que movl $0x1,(%esp) hace, además un byte adicional para el desplazamiento 0x4 .


De hecho, aquí hay una sesión de depuración que muestra lo que quiero decir:


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> _

Puede ver que la segunda instrucción con un desplazamiento en realidad es diferente a la primera sin él. Es un byte más largo (5 bytes en lugar de 4, para mantener el desplazamiento) y en realidad tiene una codificación diferente c745 en lugar de c705 .


También puede ver que puede codificar la primera y la tercera instrucción de dos maneras diferentes, pero básicamente hacen lo mismo.



El and $0xfffffff0,%esp instrucción es una forma de forzar esp estar en un límite específico. Esto se utiliza para garantizar la alineación adecuada de las variables. Muchos accesos a la memoria en los procesadores modernos serán más eficientes si siguen las reglas de alineación (como un valor de 4 bytes que debe alinearse con un límite de 4 bytes). Algunos procesadores modernos incluso generarán una falla si no sigue estas reglas.


Después de esta instrucción, tiene la garantía de que esp es menor o igual que su valor anterior y alineado a un límite de 16 bytes.



El gs: prefijo simplemente significa usar el gs registro de segmento para acceder a la memoria en lugar del predeterminado.


La instrucción mov %eax,-0xc(%ebp) significa tomar el contenido del ebp registrar, restar 12 (0xc ) y luego poner el valor de eax en esa ubicación de memoria.



Re la explicación del código. Tu function La función es básicamente un gran no-op. El ensamblaje generado se limita a la configuración y el desmontaje del marco de pila, junto con algunas comprobaciones de corrupción de marcos de pila que utilizan el %gs:14 mencionado anteriormente. ubicación de la memoria.


Carga el valor desde esa ubicación (probablemente algo como 0xdeadbeef ) en el marco de la pila, hace su trabajo y luego verifica la pila para asegurarse de que no se haya dañado.


Su trabajo, en este caso, es nada. Así que todo lo que ves es el material de administración de funciones.


La configuración de la pila se produce entre function+0 y function+12 . Todo lo que sigue es configurar el código de retorno en eax y derribando el marco de la pila, incluida la verificación de corrupción.


Del mismo modo, main consiste en la configuración del marco de pila, empujando los parámetros para function , llamando a function , derribando el marco de la pila y saliendo.


Se han insertado comentarios en el siguiente código:


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.


Creo que el motivo del %gs:0x14 puede ser evidente desde arriba pero, por si acaso, lo explicaré aquí.


Utiliza este valor (un centinela) para colocarlo en el marco de la pila actual, de modo que si algo en la función hace algo tonto como escribir 1024 bytes en una matriz de 20 bytes creada en la pila o, en su caso:


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

luego, el centinela se sobrescribirá y la verificación al final de la función lo detectará, llamando a la función de falla para informarle y luego probablemente abortando para evitar cualquier otro problema.


Si colocó 0xdeadbeef en la pila y esto se cambió a otra cosa, luego un xor con 0xdeadbeef produciría un valor distinto de cero que se detecta en el código con el je instrucción.


El bit relevante se parafrasea aquí:


          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.