Pregunta

Actualmente estoy jugando con el ensamblaje ARM en Linux como un ejercicio de aprendizaje. Estoy usando un ensamblaje 'pelado', es decir, no libcrt o libgcc. ¿Alguien puede señalarme información sobre el estado del puntero de la pila y otros registros al inicio del programa antes de que se llame a la primera instrucción? Obviamente, pc / r15 apunta a _start, y el resto parece estar inicializado a 0, con dos excepciones; sp / r13 apunta a una dirección muy lejos de mi programa, y ??r1 apunta a una dirección ligeramente más alta.

Entonces a algunas preguntas sólidas:

  • ¿Cuál es el valor en r1?
  • ¿El valor en sp es una pila legítima asignada por el kernel?
  • Si no, ¿cuál es el método preferido para asignar una pila? usando brk o asignar una sección estática .bss?

Cualquier puntero sería apreciado.

¿Fue útil?

Solución

Esto es lo que uso para iniciar un programa Linux / ARM con mi compilador:

/** 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"
);

Como puedes ver, solo obtengo las cosas argc, argv y environ de la pila en [sp].

Una pequeña aclaración: el puntero de la pila apunta a un área válida en la memoria del proceso. r0, r1, r2 y r3 son los primeros tres parámetros para la función que se llama. Los relleno con argc, argv y ambient, respectivamente.

Otros consejos

Ya que esto es Linux, puedes ver cómo está implementado por el kernel.

Los registros parecen estar configurados por la llamada a start_thread al final de load_elf_binary (si está utilizando un sistema Linux moderno, casi siempre utilizará el formato ELF). Para ARM, los registros parecen estar establecidos de la siguiente manera:

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

Claramente tienes una pila válida. Creo que los valores de r0 - r2 son basura, y en su lugar deberías leer todo de la pila (verás por qué pienso esto más adelante). Ahora, veamos lo que está en la pila. Lo que leerá de la pila se rellena con create_elf_tables .

Una cosa interesante a notar aquí es que esta función es independiente de la arquitectura, por lo que las mismas cosas (en su mayoría) se colocarán en la pila en cada arquitectura de Linux basada en ELF. Lo siguiente está en la pila, en el orden en que lo leería:

  • El número de parámetros (esto es argc en main () ).
  • Un puntero a una cadena C para cada parámetro, seguido de un cero (este es el contenido de argv en main () ; argv señalaría el primero de estos punteros).
  • Un puntero a una cadena C para cada variable de entorno, seguido de un cero (este es el contenido del tercer parámetro envp raramente visto de main () ; envp apuntaría al primero de estos punteros).
  • El " vector auxiliar " ;, que es una secuencia de pares (un tipo seguido por un valor), terminado por un par con un cero ( AT_NULL ) en el primer elemento. Este vector auxiliar tiene información interesante y útil, que puede ver (si está usando glibc) ejecutando cualquier programa vinculado dinámicamente con la variable de entorno LD_SHOW_AUXV establecida en 1 (por ejemplo, LD_SHOW_AUXV = 1 / bin / true ). Aquí también es donde las cosas pueden variar un poco dependiendo de la arquitectura.

Como esta estructura es la misma para todas las arquitecturas, puede buscar, por ejemplo, el dibujo en la página 54 de SYSV 386 ABI para tener una mejor idea de cómo encajan las cosas (sin embargo, tenga en cuenta que las constantes de tipo de vector auxiliar en ese documento son diferentes de las que usa Linux, por lo que debería mirar Linux). encabezados para ellos).

Ahora puede ver por qué los contenidos de r0 - r2 son basura. La primera palabra en la pila es argc , la segunda es un puntero al nombre del programa ( argv [0] ), y la tercera probablemente fue cero para ti porque llamaste el programa sin argumentos (sería argv [1] ). Supongo que están configurados de esta manera para el antiguo formato binario a.out , que como puede ver en create_aout_tables pone argc , argv , y envp en la pila (para que terminen en r0 - r2 en el orden esperado para una llamada a main () ).

Finalmente, ¿por qué fue r0 cero para usted en lugar de uno ( argc debería ser uno si llamó al programa sin argumentos)? Supongo que algo profundo en la maquinaria de syscall lo sobrescribió con el valor de retorno de la llamada al sistema (que sería cero ya que el ejecutivo tuvo éxito). Puede verlo en kernel_execve (que no usa la maquinaria de syscall, ya que es lo que el kernel llama cuando quiere ejecutar desde el modo kernel) que sobrescribe deliberadamente r0 con el valor de retorno de do_execve .

Aquí está el uClibc crt . Parece sugerir que todos los registros están indefinidos excepto r0 (que contiene un puntero a función para registrarse con atexit () ) y sp que contiene una dirección de pila válida.

Por lo tanto, el valor que ve en r1 probablemente no sea algo en lo que pueda confiar.

Algunos datos se colocan en la pila por usted.

Nunca he usado ARM Linux, pero le sugiero que mire la fuente de libcrt y vea lo que hacen, o use gdb para ingresar a un ejecutable existente. No debería necesitar el código fuente solo paso a través del código de ensamblaje.

Todo lo que necesita descubrir debe ocurrir dentro del primer código ejecutado por cualquier ejecutable binario.

Espero que esto ayude.

Tony

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top