gcc - ¿Cómo usar correctamente un script de enlazador simple? El ejecutable obtiene SIGKILL cuando se ejecuta
linker linker-scripts (1)
Intento entender el proceso de vinculación más profundo y los scripts del enlazador ... mirando el documento de binutils encontré una sencilla implementación del script del enlazador que he mejorado agregando algunos comandos:
OUTPUT_FORMAT("elf32-i386", "elf32-i386",
"elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(mymain)
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
Mi programa es un programa muy simple:
void mymain(void)
{
int a;
a++;
}
Ahora traté de construir un ejecutable:
gcc -c main.c
ld -o prog -T my_script.lds main.o
Pero si intento ejecutar prog
, recibe un SIGKILL
durante el inicio. Sé que cuando un programa se compila y se vincula con el comando:
gcc prog.c -o prog
el ejecutable final es también el producto de otros archivos objeto como crt1.o
, crti.o
y crtn.o
pero ¿qué pasa con mi caso? ¿Cuál es la forma correcta de utilizar estos scripts de enlazador?
Sospecho que tu código funciona bien y te metes en problemas al final: ¿qué esperas que suceda después del a++
?
mymain()
es solo una función C normal, que intentará regresar a su llamador.
Pero lo ha establecido como el punto de entrada ELF, que le dice al cargador ELF que salte a él una vez que haya cargado los segmentos del programa en el lugar correcto, y no espera que regrese.
Esos "otros archivos objeto como crt1.o
, crti.o
y crtn.o
" normalmente manejan esto para programas C. El punto de entrada ELF para un programa C no es main()
; en cambio, es un contenedor que configura un entorno apropiado para main()
(por ejemplo, configura los argumentos argc
y argv
en la pila o en los registros, dependiendo de la plataforma) , llama a main()
(con la expectativa de que pueda regresar), y luego invoca la llamada al sistema de exit
(con el código de retorno de main()
).
[Actualizar los siguientes comentarios:]
Cuando pruebo tu ejemplo con gdb
, veo que sí falla al regresar de mymain()
: después de establecer un punto de interrupción en mymain
, y luego de recorrer las instrucciones, veo que realiza el incremento, luego se mete en problemas en la función epílogo:
$ gcc -g -c main.c
$ ld -o prog -T my_script.lds main.o
$ gdb ./prog
...
(gdb) b mymain
Breakpoint 1 at 0x10006: file main.c, line 4.
(gdb) r
Starting program: /tmp/prog
Breakpoint 1, mymain () at main.c:4
4 a++;
(gdb) display/i $pc
1: x/i $pc
0x10006 <mymain+6>: addl $0x1,-0x4(%ebp)
(gdb) si
5 }
1: x/i $pc
0x1000a <mymain+10>: leave
(gdb) si
Cannot access memory at address 0x4
(gdb) si
0x00000001 in ?? ()
1: x/i $pc
Disabling display 1 to avoid infinite recursion.
0x1: Cannot access memory at address 0x1
(gdb) q
Para i386 al menos, el cargador ELF configura una pila sensible antes de ingresar el código cargado, por lo que puede establecer el punto de entrada ELF en una función C y obtener un comportamiento razonable; Sin embargo, como mencioné anteriormente, debe manejar un proceso de limpieza. Y si no está utilizando el tiempo de ejecución de C, es mejor que no use ninguna biblioteca que dependa del tiempo de ejecución de C tampoco.
Así que aquí hay un ejemplo de eso, usando su secuencia de comandos del enlazador original, pero con el código C modificado para inicializar a
valor conocido e invocar una llamada al sistema de exit
(usando ensamblaje en línea) con el valor final de a
como código de salida. (Nota: Me acabo de dar cuenta de que no me has dicho exactamente qué plataforma estás usando, supongo que aquí Linux).
$ cat main2.c
void mymain(void)
{
int a = 42;
a++;
asm volatile("mov $1,%%eax; mov %0,%%ebx; int $0x80" : : "r"(a) : "%eax" );
}
$ gcc -c main2.c
$ ld -o prog2 -T my_script.lds main2.o
$ ./prog2 ; echo $?
43
$