Su gcc está creando ejecutables PIE de forma predeterminada (¿las direcciones absolutas de 32 bits ya no se permiten en x86-64 Linux?).
No estoy seguro de por qué, pero al hacerlo, el enlazador no resuelve automáticamente call puts
a call [email protected]
. Todavía hay un puts
Entrada PLT generada, pero el call
no va allí.
En tiempo de ejecución, el enlazador dinámico intenta resolver puts
directamente al símbolo libc de ese nombre y corrija el call rel32
. Pero el símbolo está a más de +-2^31 de distancia, por lo que recibimos una advertencia sobre el desbordamiento del R_X86_64_PC32
reubicación Los 32 bits inferiores de la dirección de destino son correctos, pero los bits superiores no lo son. (Por lo tanto, su call
salta a una dirección incorrecta).
Tu código funciona para mí si construyo con gcc -no-pie -fno-pie call-lib.c libcall.o
. El -no-pie
es la parte crítica:es la opción del enlazador. Su comando YASM no tiene que cambiar.
Al crear un ejecutable tradicional dependiente de la posición, el enlazador convierte el puts
símbolo para el destino de la llamada en [email protected]
para usted, porque estamos vinculando un ejecutable dinámico (en lugar de vincular estáticamente libc con gcc -static -fno-pie
, en cuyo caso el call
podría ir directamente a la función libc.)
De todos modos, esta es la razón por la que gcc emite call [email protected]
(sintaxis GAS) al compilar con -fpie
(el predeterminado en su escritorio, pero no el predeterminado en https://godbolt.org/), pero solo call puts
al compilar con -fno-pie
.
Vea ¿Qué significa @plt aquí? para obtener más información sobre el PLT, y también el estado de las bibliotecas dinámicas en Linux desde hace unos años. (El moderno gcc -fno-plt
es como una de las ideas en esa publicación de blog).
Por cierto, un prototipo más preciso/específico permitiría a gcc evitar poner a cero EAX antes de llamar a foo
:
extern void foo();
en C significa extern void foo(...);
Podrías declararlo como extern void foo(void);
, que es lo que ()
significa en C++. C++ no permite declaraciones de funciones que dejen los argumentos sin especificar.
mejoras de asm
También puedes poner message
en section .rodata
(datos de solo lectura, vinculados como parte del segmento de texto).
No necesita un marco de pila, solo algo para alinear la pila en 16 antes de una llamada. Un muñeco push rax
lo hará.
O podemos llamar a la cola puts
por saltando a él en lugar de llamarlo, con la misma posición de pila que en la entrada a esta función. Esto funciona con o sin PIE. Simplemente reemplace call
con jmp
, siempre que RSP apunte a su propia dirección de devolución.
Si desea crear ejecutables PIE (o bibliotecas compartidas), tiene dos opciones
call puts wrt ..plt
- llamar explícitamente a través del PLT.call [rel puts wrt ..got]
- haz explícitamente una llamada indirecta a través de la entrada GOT, como-fno-plt
de gcc estilo de código-gen. (Usando un modo de direccionamiento relativo a RIP para llegar al GOT, de ahí elrel
palabra clave).
WRT =Con respecto a. Los documentos del manual NASM wrt ..plt
, y consulte también la sección 7.9.3:símbolos especiales y WRT.
Normalmente usarías default rel
en la parte superior de su archivo para que pueda usar call [puts wrt ..got]
y todavía obtener un modo de direccionamiento relativo a RIP. No puede usar un modo de direccionamiento absoluto de 32 bits en código PIE o PIC.
call [puts wrt ..got]
se ensambla en una llamada indirecta a la memoria utilizando el puntero de función que el enlace dinámico almacena en GOT. (Enlace anticipado, no enlace dinámico perezoso).
Documentos NASM ..got
para obtener la dirección de las variables en la sección 9.2.3. Las funciones en (otras) bibliotecas son idénticas:obtiene un puntero de GOT en lugar de llamar directamente, porque el desplazamiento no es una constante de tiempo de enlace y es posible que no quepa en 32 bits.
YASM también acepta call [puts wrt ..GOTPCREL]
, como la sintaxis de AT&T call *[email protected](%rip)
, pero NASM no.
; don't use BITS 64. You *want* an error if you try to assemble this into a 32-bit .o
default rel ; RIP-relative addressing instead of 32-bit absolute by default; makes the [rel ...] optional
section .rodata ; .rodata is best for constants, not .data
message:
db 'foo() called', 0
section .text
global foo
foo:
sub rsp, 8 ; align the stack by 16
; PIE with PLT
lea rdi, [rel message] ; needed for PIE
call puts WRT ..plt ; tailcall puts
;or
; PIE with -fno-plt style code, skips the PLT indirection
lea rdi, [rel message]
call [rel puts wrt ..got]
;or
; non-PIE
mov edi, message ; more efficient, but only works in non-PIE / non-PIC
call puts ; linker will rewrite it into call [email protected]
add rsp,8 ; remove the padding
ret
En una posición-dependiente ejecutable, puede usar mov edi, message
en lugar de un LEA relativo a RIP. Tiene un tamaño de código más pequeño y puede ejecutarse en más puertos de ejecución en la mayoría de las CPU.
En un ejecutable que no sea PIE, también podría usar call puts
o jmp puts
y deje que el enlazador lo resuelva, a menos que desee un enlace dinámico de estilo sin plt más eficiente. Pero si elige vincular estáticamente libc, creo que esta es la única forma en que obtendrá un jmp directo a la función libc.
(Creo que la posibilidad de enlaces estáticos para no PIE es por qué ld
está dispuesto a generar stubs PLT automáticamente para no PIE, pero no para PIE o bibliotecas compartidas. Requiere que diga lo que quiere decir al vincular objetos compartidos ELF).
Si usaste call puts
en un PIE (call rel32
), solo podría funcionar si vinculaste estáticamente una implementación independiente de la posición de puts
en su PIE, por lo que todo era un ejecutable que se cargaba en una dirección aleatoria en tiempo de ejecución (mediante el mecanismo habitual de vinculación dinámica), pero simplemente no dependía de libc.so.6
El 0xe8
El código de operación es seguido por un desplazamiento firmado que se aplicará a la PC (que ha avanzado a la siguiente instrucción en ese momento) para calcular el objetivo de la bifurcación. Por lo tanto objdump
está interpretando el objetivo de la rama como 0x671
.
YASM está representando ceros porque probablemente ha colocado una reubicación en ese desplazamiento, que es como le pide al cargador que complete el desplazamiento correcto para puts
durante la carga. El cargador se encuentra con un desbordamiento al calcular la reubicación, lo que puede indicar que puts
está en un desplazamiento mayor de su llamada que el que se puede representar en un desplazamiento con signo de 32 bits. Por lo tanto, el cargador no corrige esta instrucción y se bloquea.
66c: e8 00 00 00 00
muestra la dirección despoblada. Si observa su tabla de reubicación, debería ver una reubicación en 0x66d
. No es raro que el ensamblador llene direcciones/compensaciones con reubicaciones como ceros.
Esta página sugiere que YASM tiene un WRT
directiva que puede controlar el uso de .got
, .plt
, etc.
Según S9.2.5 en la documentación de NASM, parece que puede usar CALL puts WRT ..plt
(suponiendo que YASM tenga la misma sintaxis).