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

Était-ce utile?

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.

  1. 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.

  2. 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
}

DÉMONSTRATIONS

cps_alloca sur github

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).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top