Votre gcc construit des exécutables PIE par défaut (les adresses absolues 32 bits ne sont plus autorisées dans x86-64 Linux ?).
Je ne sais pas pourquoi, mais ce faisant, l'éditeur de liens ne résout pas automatiquement call puts
à call [email protected]
. Il reste un puts
Entrée PLT générée, mais le call
n'y va pas.
Lors de l'exécution, l'éditeur de liens dynamique tente de résoudre puts
directement au symbole libc de ce nom et corrigez le call rel32
. Mais le symbole est à plus de +-2^31, nous recevons donc un avertissement concernant le débordement du R_X86_64_PC32
déménagement. Les 32 bits inférieurs de l'adresse cible sont corrects, mais les bits supérieurs ne le sont pas. (Ainsi votre call
saute à une mauvaise adresse).
Votre code fonctionne pour moi si je compile avec gcc -no-pie -fno-pie call-lib.c libcall.o
. Le -no-pie
est la partie critique :c'est l'option de l'éditeur de liens. Votre commande YASM n'a pas à changer.
Lors de la création d'un exécutable traditionnel dépendant de la position, l'éditeur de liens transforme le puts
symbole de la cible de l'appel en [email protected]
pour vous, car nous lions un exécutable dynamique (au lieu de lier statiquement libc avec gcc -static -fno-pie
, auquel cas le call
pourrait aller directement à la fonction libc.)
Quoi qu'il en soit, c'est pourquoi gcc émet call [email protected]
(syntaxe GAS) lors de la compilation avec -fpie
(la valeur par défaut sur votre bureau, mais pas la valeur par défaut sur https://godbolt.org/), mais juste call puts
lors de la compilation avec -fno-pie
.
Voir Que signifie @plt ici ? pour en savoir plus sur le PLT, et aussi Désolé l'état des bibliothèques dynamiques sur Linux il y a quelques années. (Le gcc -fno-plt
moderne est comme l'une des idées de ce billet de blog.)
BTW, un prototype plus précis/spécifique permettrait à gcc d'éviter de mettre à zéro EAX avant d'appeler foo
:
extern void foo();
en C signifie extern void foo(...);
Vous pouvez le déclarer comme extern void foo(void);
, c'est ce que ()
signifie en C++. C++ n'autorise pas les déclarations de fonction qui laissent les arguments non spécifiés.
améliorations asm
Vous pouvez aussi mettre message
en section .rodata
(données en lecture seule, liées dans le cadre du segment de texte).
Vous n'avez pas besoin d'un cadre de pile, juste quelque chose pour aligner la pile par 16 avant un appel. Un mannequin push rax
va le faire.
Ou nous pouvons appeler le puts
en sautant au lieu de l'appeler, avec la même position dans la pile qu'à l'entrée de cette fonction. Cela fonctionne avec ou sans PIE. Remplacez simplement call
avec jmp
, tant que RSP pointe vers votre propre adresse de retour.
Si vous souhaitez créer des exécutables PIE (ou des bibliothèques partagées), vous avez deux options
call puts wrt ..plt
- appeler explicitement via le PLT.call [rel puts wrt ..got]
- faire explicitement un appel indirect via l'entrée GOT, comme le-fno-plt
de gcc style de code-gen. (Utilisation d'un mode d'adressage relatif au RIP pour atteindre le GOT, d'où lerel
mot-clé).
WRT =En ce qui concerne. Le manuel NASM documente wrt ..plt
, et voir aussi section 7.9.3 :symboles spéciaux et WRT.
Normalement, vous utiliseriez default rel
en haut de votre fichier afin que vous puissiez réellement utiliser call [puts wrt ..got]
et obtenir toujours un mode d'adressage relatif au RIP. Vous ne pouvez pas utiliser un mode d'adressage absolu 32 bits dans le code PIE ou PIC.
call [puts wrt ..got]
assemble à un appel indirect en mémoire en utilisant le pointeur de fonction que la liaison dynamique stockée dans le GOT. (Liaison anticipée, pas de liaison dynamique paresseuse.)
Documents MSNA ..got
pour obtenir l'adresse des variables dans la section 9.2.3. Les fonctions des (autres) bibliothèques sont identiques :vous obtenez un pointeur du GOT au lieu d'appeler directement, car le décalage n'est pas une constante de temps de liaison et peut ne pas tenir en 32 bits.
YASM accepte également call [puts wrt ..GOTPCREL]
, comme la syntaxe AT&T call *[email protected](%rip)
, mais pas la NASM.
; 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
Dans un poste-dépendant exécutable, vous pouvez utiliser mov edi, message
au lieu d'un LEA relatif à RIP. Sa taille de code est plus petite et peut s'exécuter sur plus de ports d'exécution sur la plupart des processeurs.
Dans un exécutable non-PIE, vous pouvez également utiliser call puts
ou jmp puts
et laissez l'éditeur de liens régler le problème, à moins que vous ne souhaitiez une liaison dynamique de style no-plt plus efficace. Mais si vous choisissez de lier statiquement libc, je pense que c'est la seule façon d'obtenir un jmp direct vers la fonction libc.
(Je pense que la possibilité d'un lien statique pour les non-PIE est pourquoi ld
est prêt à générer automatiquement des stubs PLT pour les non-PIE, mais pas pour les PIE ou les bibliothèques partagées. Cela vous oblige à dire ce que vous voulez dire lorsque vous liez des objets partagés ELF.)
Si vous avez utilisé call puts
dans un PIE (call rel32
), cela ne peut fonctionner que si vous avez lié statiquement une implémentation indépendante de la position de puts
dans votre PIE, donc le tout était un exécutable qui serait chargé à une adresse aléatoire au moment de l'exécution (par le mécanisme de liaison dynamique habituel), mais n'avait tout simplement pas de dépendance sur libc.so.6
Le 0xe8
opcode est suivi d'un décalage signé à appliquer au PC (qui est passé à l'instruction suivante à ce moment-là) pour calculer la cible de branchement. D'où objdump
interprète la cible de branche comme 0x671
.
YASM rend des zéros car il a probablement mis un déplacement sur ce décalage, c'est ainsi qu'il demande au chargeur de remplir le décalage correct pour puts
pendant le chargement. Le chargeur rencontre un débordement lors du calcul de la relocalisation, ce qui peut indiquer que puts
est à un décalage plus éloigné de votre appel que ce qui peut être représenté dans un décalage signé 32 bits. Par conséquent, le chargeur ne parvient pas à corriger cette instruction et vous obtenez un plantage.
66c: e8 00 00 00 00
affiche l'adresse vide. Si vous regardez dans votre tableau de relocalisation, vous devriez voir une relocalisation sur 0x66d
. Il n'est pas rare que l'assembleur remplisse les adresses/décalages avec des relocalisations comme tous les zéros.
Cette page suggère que YASM a un WRT
directive qui peut contrôler l'utilisation de .got
, .plt
, etc.
Selon S9.2.5 sur la documentation NASM, il semble que vous puissiez utiliser CALL puts WRT ..plt
(en supposant que YASM a la même syntaxe).