la mise en œuvre alloca
-
23-08-2019 - |
Question
Comment peut-on mettre en œuvre alloca () en utilisant assembleur en ligne x86 dans des langues comme D, C, et C ++? Je veux créer une version légèrement modifiée de celui-ci, mais d'abord je dois savoir comment est mis en œuvre la version standard. La lecture du démontage de compilateurs ne permet pas parce qu'ils effectuent tant d'optimisations, et je veux juste la forme canonique.
Edit: Je suppose que le plus dur est que je veux que cela ait la syntaxe d'appel de fonction, à savoir l'aide d'une fonction nue ou quelque chose, la faire ressembler à la normale alloca ()
.Edit # 2:. Ah, ce que le diable, vous pouvez supposer que nous ne sommes pas omettre le pointeur de cadre
La solution
la mise en œuvre alloca
fait a besoin d'aide du compilateur . Quelques personnes ici disent qu'il est aussi simple que:
sub esp, <size>
qui est malheureusement seulement la moitié de l'image. Oui, ce serait « Allouer de l'espace sur la pile », mais il y a quelques gotchas.
-
si le compilateur avait de code émis qui fait référence à d'autres variables par rapport au lieu de
esp
ebp
(Typique si vous compilez sans pointeur de trame). ensuite, les les références doivent être ajustées. Même avec des pointeurs de trame, les compilateurs font parfois. -
plus important encore, par définition, l'espace alloué avec
alloca
doit être "Libéré" lorsque les sorties de fonction.
Le grand est le point n ° 2. Parce que vous besoin le compilateur à émettre code pour ajouter symétriquement <size>
à esp
à chaque point de sortie de la fonction.
Le cas le plus probable est le compilateur offre quelques intrinsics qui permettent les auteurs de bibliothèque pour demander au compilateur de l'aide nécessaire.
EDIT:
En fait, dans la glibc (mise en œuvre de GNU libc). La mise en œuvre de alloca
est simplement ceci:
#ifdef __GNUC__
# define __alloca(size) __builtin_alloca (size)
#endif /* GCC. */
EDIT:
après réflexion, le minimum que je crois serait nécessaire serait pour le compilateur toujours utiliser un pointeur de cadre dans toutes les fonctions qui utilise alloca
, quels que soient les paramètres d'optimisation. Cela permettrait à toutes les sections locales à être référencés par ebp
en toute sécurité et le nettoyage du cadre serait traitée en rétablissant le pointeur de trame à esp
.
EDIT:
Alors j'ai fait des expériences avec des choses comme ceci:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define __alloca(p, N) \
do { \
__asm__ __volatile__( \
"sub %1, %%esp \n" \
"mov %%esp, %0 \n" \
: "=m"(p) \
: "i"(N) \
: "esp"); \
} while(0)
int func() {
char *p;
__alloca(p, 100);
memset(p, 0, 100);
strcpy(p, "hello world\n");
printf("%s\n", p);
}
int main() {
func();
}
qui, malheureusement, ne fonctionne pas correctement. Après analyse de la sortie du dispositif par gcc. Il semble que les optimisations obtenir de la manière. Le problème semble être que, puisque l'optimiseur du compilateur est tout à fait au courant de mon montage en ligne, il a l'habitude de faire les choses dans un ordre inattendu et toujours référencement des choses par esp
.
Voici l'ASM qui en résulte:
8048454: push ebp
8048455: mov ebp,esp
8048457: sub esp,0x28
804845a: sub esp,0x64 ; <- this and the line below are our "alloc"
804845d: mov DWORD PTR [ebp-0x4],esp
8048460: mov eax,DWORD PTR [ebp-0x4]
8048463: mov DWORD PTR [esp+0x8],0x64 ; <- whoops! compiler still referencing via esp
804846b: mov DWORD PTR [esp+0x4],0x0 ; <- whoops! compiler still referencing via esp
8048473: mov DWORD PTR [esp],eax ; <- whoops! compiler still referencing via esp
8048476: call 8048338 <memset@plt>
804847b: mov eax,DWORD PTR [ebp-0x4]
804847e: mov DWORD PTR [esp+0x8],0xd ; <- whoops! compiler still referencing via esp
8048486: mov DWORD PTR [esp+0x4],0x80485a8 ; <- whoops! compiler still referencing via esp
804848e: mov DWORD PTR [esp],eax ; <- whoops! compiler still referencing via esp
8048491: call 8048358 <memcpy@plt>
8048496: mov eax,DWORD PTR [ebp-0x4]
8048499: mov DWORD PTR [esp],eax ; <- whoops! compiler still referencing via esp
804849c: call 8048368 <puts@plt>
80484a1: leave
80484a2: ret
Comme vous pouvez le voir, il est pas si simple. Malheureusement, je maintiens mon affirmation initiale que vous avez besoin d'aide du compilateur.
Autres conseils
Il serait difficile de le faire - en fait, à moins que vous avez assez de contrôle sur la génération de code du compilateur, il ne peut se faire entièrement en toute sécurité. Votre routine devrait manipuler la pile, de sorte que quand il est revenu tout a été nettoyé, mais le pointeur de la pile est restée dans une position telle que le bloc de mémoire est resté à cet endroit.
Le problème est que si vous pouvez informer le compilateur que le pointeur de la pile est a été modifié dans votre appel de fonction, il peut bien décider qu'il peut continuer à se référer à d'autres sections locales (ou autre) par le pointeur de la pile - mais la les compensations seront incorrectes.
Pour le langage de programmation D, le code source pour alloca () est livré avec le télécharger . Comment ça marche est assez bien commenté. Pour dmd1, il est en /dmd/src/phobos/internal/alloca.d. Pour dmd2, il est en /dmd/src/druntime/src/compiler/dmd/alloca.d.
C et C ++ normes ne précise pas que alloca()
a à l'utilisation de la pile, car alloca()
se trouve pas dans le C ou C ++ standards (ou POSIX pour cette matière) ¹.
Un compilateur peut également mettre en œuvre alloca()
en utilisant le tas. Par exemple, le compilateur alloca()
de RealView ARM (RVCT) utilise malloc()
pour allouer la mémoire tampon ( _malloca()
fonction qui utilise le tas s'il n'y a pas assez de place sur la pile, mais il faut l'appelant d'utiliser _freea()
, contrairement _alloca()
, qui n'a pas besoin / veulent libération explicite.
(avec C ++ Destructeurs à votre disposition, vous pouvez évidemment faire le nettoyage sans l'aide du compilateur, mais vous ne pouvez pas déclarer des variables locales dans une expression arbitraire, donc je ne pense pas que vous pouvez écrire une macro alloca()
qui utilise RAII. Ensuite, encore une fois, vous pouvez apparemment pas utiliser alloca()
dans certaines expressions (comme ) de toute façon .)
Oui ¹, il est possible d'écrire un alloca()
qui appelle simplement system("/usr/games/nethack")
.
alloca est directement mis en œuvre dans le code assembleur. C'est parce que vous ne pouvez pas contrôler la mise en page de la pile directement à partir de langages de haut niveau.
Notez également que la plupart la mise en œuvre effectuera une optimisation supplémentaire comme l'alignement de la pile pour des raisons de performances. La méthode standard d'allocation d'espace de pile sur X86 ressemble à ceci:
sub esp, XXX
Alors que XXX est le nombre d'octets à allcoate
Edit:
Si vous voulez regarder la mise en œuvre (et que vous utilisez MSVC) voir alloca16.asm et chkstk.asm.
Le code dans le premier fichier aligne essentiellement la taille d'allocation désirée à une limite de 16 octets. Code dans le 2ème fichier marche réellement toutes les pages qui appartiennent à la nouvelle zone de pile et les touches. Cela peut déclencher des exceptions PAGE_GAURD qui sont utilisées par le système d'exploitation pour développer la pile.
Poursuite Style de passes Alloca
Tableau de longueur variable pur ISO C ++ . la mise en œuvre de preuve de concept.
Utilisation
void foo(unsigned n)
{
cps_alloca<Payload>(n,[](Payload *first,Payload *last)
{
fill(first,last,something);
});
}
Idée de base
template<typename T,unsigned N,typename F>
auto cps_alloca_static(F &&f) -> decltype(f(nullptr,nullptr))
{
T data[N];
return f(&data[0],&data[0]+N);
}
template<typename T,typename F>
auto cps_alloca_dynamic(unsigned n,F &&f) -> decltype(f(nullptr,nullptr))
{
vector<T> data(n);
return f(&data[0],&data[0]+n);
}
template<typename T,typename F>
auto cps_alloca(unsigned n,F &&f) -> decltype(f(nullptr,nullptr))
{
switch(n)
{
case 1: return cps_alloca_static<T,1>(f);
case 2: return cps_alloca_static<T,2>(f);
case 3: return cps_alloca_static<T,3>(f);
case 4: return cps_alloca_static<T,4>(f);
case 0: return f(nullptr,nullptr);
default: return cps_alloca_dynamic<T>(n,f);
}; // mpl::for_each / array / index pack / recursive bsearch / etc variacion
}
Vous pouvez examiner les sources d'un compilateur C open-source, comme Ouvrir Watcom et trouver vous-même
Si vous ne pouvez pas utiliser des tableaux de longueur variable de C99, vous pouvez utiliser un casting littéral composé à un pointeur vide.
#define ALLOCA(sz) ((void*)((char[sz]){0}))
Ceci fonctionne également pour -ansi (comme une extension gcc) et même quand il est un argument de fonction;
some_func(&useful_return, ALLOCA(sizeof(struct useless_return)));
L'inconvénient est que lorsque compilé en C ++, g ++> 4.6 vous donnera un erreur: prendre l'adresse du tableau temporaire ... clang et cpi ne se plaignent pas si
Ce que nous voulons faire est quelque chose comme ça:
void* alloca(size_t size) {
<sp> -= size;
return <sp>;
}
Dans l'Assemblée (Visual Studio 2017, 64 bits), il ressemble à:
;alloca.asm
_TEXT SEGMENT
PUBLIC alloca
alloca PROC
sub rsp, rcx ;<sp> -= size
mov rax, rsp ;return <sp>;
ret
alloca ENDP
_TEXT ENDS
END
Malheureusement, notre pointeur de retour est le dernier élément de la pile, et nous ne voulons pas l'écraser. De plus, nous devons prendre soin de l'alignement, à savoir. tour Taille au multiple de 8. Nous devons donc faire ceci:
;alloca.asm
_TEXT SEGMENT
PUBLIC alloca
alloca PROC
;round up to multiple of 8
mov rax, rcx
mov rbx, 8
xor rdx, rdx
div rbx
sub rbx, rdx
mov rax, rbx
mov rbx, 8
xor rdx, rdx
div rbx
add rcx, rdx
;increase stack pointer
pop rbx
sub rsp, rcx
mov rax, rsp
push rbx
ret
alloca ENDP
_TEXT ENDS
END
Alloca est facile, vous déplacez le curseur de la pile jusqu'à; puis générer toute la lecture / écriture pour pointer vers ce nouveau bloc
sub esp, 4
Je recommande l'instruction "enter". Disponible sur 286 et les nouveaux processeurs ( peut ont été disponibles sur le 186 ainsi, je ne me souviens pas désinvolture, mais ce ne sont pas largement disponibles de toute façon).