Gestion des objets C ++ dans un tampon, en tenant compte des hypothèses d'alignement et de présentation de la mémoire

StackOverflow https://stackoverflow.com/questions/417446

Question

Je stocke des objets dans un tampon. Maintenant, je sais que je ne peux pas présumer de la disposition de la mémoire de l'objet.

Si je connais la taille globale de l'objet, est-il acceptable de créer un pointeur sur cette mémoire et d'appeler des fonctions dessus?

par exemple. dis que j'ai la classe suivante:

[int,int,int,int,char,padding*3bytes,unsigned short int*]

1) si je connais cette classe comme étant de taille 24 et que je connais l'adresse d'où elle commence en mémoire Bien qu'il ne soit pas prudent de supposer que la disposition de la mémoire est acceptable, elle peut être convertie en un pointeur et appeler des fonctions sur cet objet qui accède à ces membres. (Est-ce que c ++ connaît par magie la position correcte d'un membre?)

2) Si cela n’est pas sûr / ok, existe-t-il un autre moyen que d’utiliser un constructeur qui prend tous les arguments et extrait chaque argument de la mémoire tampon un par un?

Modifier: modification du titre pour le rendre plus approprié à ce que je demande.

Était-ce utile?

La solution

Vous pouvez créer un constructeur qui prend tous les membres et les affecte, puis utilise le placement nouveau.

class Foo
{
    int a;int b;int c;int d;char e;unsigned short int*f;
public:
    Foo(int A,int B,int C,int D,char E,unsigned short int*F) : a(A), b(B), c(C), d(D), e(E), f(F) {}
};

...
char *buf  = new char[sizeof(Foo)];   //pre-allocated buffer
Foo *f = new (buf) Foo(a,b,c,d,e,f);

Cela a l’avantage que même la v-table sera générée correctement. Notez, cependant, que si vous utilisez ceci pour la sérialisation, le pointeur unsigned short int ne pointe vers rien d’utile lorsque vous le désérialisez, à moins que vous ne preniez très soin d’utiliser une sorte de méthode pour convertir les pointeurs en décalages, puis de nouveau .

Les méthodes individuelles sur un pointeur this sont statiquement liées et constituent simplement un appel direct à la fonction, this étant le premier paramètre avant les paramètres explicites.

Les variables membres sont référencées en utilisant un décalage par rapport au pointeur this . Si un objet est disposé comme ceci:

0: vtable
4: a
8: b
12: c
etc...

un sera accessible par déréférencement this + 4 octets .

Autres conseils

Ce que vous proposez de faire est de lire une série d'octets (espérons-le pas aléatoire), de les convertir en un objet connu, puis d'appeler une méthode de classe sur cet objet. Cela pourrait fonctionner, car ces octets vont se retrouver dans le "this" pointeur dans cette méthode de classe. Mais vous prenez une chance réelle que les choses ne soient pas où le code compilé s'attend à ce qu'il soit. Et contrairement à Java ou à C #, il n’existe pas de véritable "runtime". pour résoudre ce type de problèmes, vous obtiendrez au mieux un vidage mémoire, et au pire, vous obtiendrez une mémoire corrompue.

On dirait que vous voulez une version C ++ de la sérialisation / désérialisation de Java. Il existe probablement une bibliothèque pour le faire.

Les appels de fonctions non virtuelles sont liés directement, comme une fonction C. Le pointeur d'objet (this) est passé en tant que premier argument. Aucune connaissance de la disposition de l'objet n'est requise pour appeler la fonction.

On dirait que vous ne stockez pas les objets eux-mêmes dans un tampon, mais plutôt les données dont ils font partie.

Si ces données sont en mémoire dans l'ordre dans lequel les champs sont définis dans votre classe (avec un remplissage approprié pour la plate-forme) et , votre type est un POD , vous pouvez mémoriser> les données de la tampon vers un pointeur de votre type (ou éventuellement le cast, mais attention, il y a des pièges spécifiques à la plate-forme avec des transferts vers des pointeurs de types différents).

Si votre classe n'est pas un POD, la disposition des champs en mémoire n'est pas garantie et vous ne devez vous fier à aucun ordre observé, car il est autorisé à changer à chaque recompilation.

Vous pouvez cependant initialiser un non-POD avec les données d'un POD.

En ce qui concerne les adresses où se trouvent des fonctions non virtuelles: elles sont statiquement liées lors de la compilation à un emplacement de votre segment de code identique pour chaque instance de votre type. Notez qu'il n'y a pas de " runtime " impliqué. Lorsque vous écrivez un code comme celui-ci:

class Foo{
   int a;
   int b;

public:
   void DoSomething(int x);
};

void Foo::DoSomething(int x){a = x * 2; b = x + a;}

int main(){
    Foo f;
    f.DoSomething(42);
    return 0;
}

le compilateur génère du code qui fait quelque chose comme ceci:

  1. fonction main :
    1. allouez 8 octets sur la pile pour l'objet " f "
    2. appelez l'initialiseur par défaut pour la classe " Foo " (ne fait rien dans ce cas)
    3. pousser la valeur de l'argument 42 sur la pile
    4. pointeur sur l'objet "" f " sur la pile
    5. appeler la fonction Foo_i_DoSomething @ 4 (le nom réel est généralement plus complexe)
    6. charger la valeur de retour 0 dans le registre d'accumulateur
    7. retourner à l'appelant
  2. fonction Foo_i_DoSomething @ 4 (situé ailleurs dans le segment de code)
    1. charger & <; code> x " valeur de la pile (poussée par l'appelant)
    2. multiplier par 2
    3. charger " ce " pointeur de la pile (poussé par l'appelant)
    4. calculez le décalage du champ " a " dans un objet Foo
    5. ajouter un décalage calculé à ce pointeur chargé à l'étape 3
    6. produit en magasin, calculé à l'étape 2, à compenser calculé à l'étape 5
    7. charger & <; code> x " valeur de la pile, encore une fois
    8. charger " ce " pointeur de la pile, encore une fois
    9. calculez le décalage du champ " a " dans un objet Foo , encore une fois
    10. ajouter un décalage calculé à ce pointeur chargé à l'étape 8
    11. charger " a " valeur stockée au décalage,
    12. ajouter " a " valeur, chargée dans l’étape 12, sur " x " valeur chargée à l'étape 7
    13. charger " ce " pointeur de la pile, encore une fois
    14. calculez le décalage du champ " b " dans un objet Foo
    15. ajouter un décalage calculé à ce pointeur chargé à l'étape 14
    16. stocker la somme, calculée à l'étape 13, à compenser, calculée à l'étape 16
    17. retourner à l'appelant

En d'autres termes, ce serait plus ou moins le même code que si vous l'aviez écrit (les détails, tels que le nom de la fonction DoSomething et la méthode de passage de ce pointeur sont laissés au compilateur) :

class Foo{
    int a;
    int b;

    friend void Foo_DoSomething(Foo *f, int x);
};

void Foo_DoSomething(Foo *f, int x){
    f->a = x * 2;
    f->b = x + f->a;
}

int main(){
    Foo f;
    Foo_DoSomething(&f, 42);
    return 0;
}
  1. Dans ce cas, un objet de type POD est déjà créé (que vous appeliez ou non un nouvel appel. L'allocation du stockage requis suffit déjà), et vous pouvez accéder à ses membres, notamment en appelant une fonction sur celui-ci. objet. Mais cela ne fonctionnera que si vous connaissez précisément l'alignement requis de T, la taille de T (la mémoire tampon ne doit pas être inférieure à celle-ci) et l'alignement de tous les membres de T. Même pour un type de pod, le compilateur est autorisé à mettre des octets de remplissage entre les membres, s'il le souhaite. Pour les types non-POD, vous pouvez avoir la même chance si votre type n'a pas de fonctions virtuelles ou de classes de base, pas de constructeur défini par l'utilisateur (bien sûr) et que cela s'applique également à la base et à tous ses membres non statiques.

  2. Pour tous les autres types, tous les paris sont désactivés. Vous devez d'abord lire les valeurs avec un POD, puis initialiser un type non-POD avec ces données.

  

Je stocke des objets dans un tampon. ... Si je connais la taille globale de l'objet, est-il acceptable de créer un pointeur sur cette mémoire et d'appeler des fonctions dessus?

Ceci est acceptable dans la mesure où l'utilisation de conversions est acceptable:

#include <iostream>

namespace {
    class A {
        int i;
        int j;
    public:
        int value()
        {
            return i + j;
        }
    };
}

int main()
{
    char buffer[] = { 1, 2 };
    std::cout << reinterpret_cast<A*>(buffer)->value() << '\n';
}

Lancer un objet dans une mémoire telle que la mémoire brute est en fait assez commun, en particulier dans le monde C. Toutefois, si vous utilisez une hiérarchie de classes, il serait plus judicieux d’utiliser un pointeur sur les fonctions membres.

  

dis que j'ai la classe suivante: ...

     

Si je connais cette classe comme étant de taille 24 et si je connais l'adresse de départ, elle commence en mémoire ...

C’est là que les choses se compliquent. La taille d'un objet comprend la taille de ses membres de données (et de tous les membres de n'importe quelle classe de base), de tout remplissage, de tout pointeur de fonction ou de toute information dépendante de l'implémentation, moins tout élément enregistré de certaines optimisations de taille (optimisation de classe de base vide). Si le nombre résultant est 0 octet, l'objet doit prendre au moins un octet en mémoire. Ces éléments sont une combinaison de problèmes de langue et d’exigences communes à la plupart des processeurs en matière d’accès à la mémoire. Essayer de faire fonctionner les choses peut être une vraie douleur .

Si vous ne faites qu'allouer un objet et transtyper vers et depuis la mémoire brute, vous pouvez ignorer ces problèmes. Mais si vous copiez les éléments internes d'un objet dans un tampon quelconque, ils se redressent assez rapidement. Le code ci-dessus repose sur quelques règles générales concernant l’alignement (c’est-à-dire que je sais que la classe A aura les mêmes restrictions d’alignement que ints, et que le tableau peut donc être converti en un A sans risque. idem si j’ai jeté des parties du tableau sur A et des parties sur d’autres classes avec d’autres membres de données).

Oh, et lors de la copie d'objets, vous devez vous assurer de gérer correctement les pointeurs.

Vous pouvez également être intéressé par des éléments tels que les tampons de protocole de Google ou L'épargne de Facebook .

Oui, ces problèmes sont difficiles. Et, oui, certains langages de programmation les cachent. Mais il y a énormément de choses à faire balayé sous le tapis :

  

Dans la machine virtuelle HotSpot JVM de Sun, le stockage d'objets est aligné sur la limite de 64 bits la plus proche. De plus, chaque objet a un en-tête de 2 mots en mémoire. La taille des mots de la machine virtuelle Java correspond généralement à la taille du pointeur natif de la plateforme. (Un objet composé uniquement d'un int de 32 bits et d'un double de 64 bits - 96 bits de données - nécessite) deux mots pour l'en-tête de l'objet, un mot pour l'int, deux mots pour le double. C'est 5 mots: 160 bits. En raison de l'alignement, cet objet occupera 192 bits de mémoire.

C’est parce que Sun s’appuie sur une tactique relativement simple pour résoudre les problèmes d’alignement de la mémoire (sur un processeur imaginaire, un caractère peut exister à n’importe quel emplacement de la mémoire, un int à un emplacement divisible par 4 et un double). Il se peut qu’il soit nécessaire d’allouer des ressources uniquement aux emplacements mémoire divisibles par 32, mais les exigences d’alignement les plus contraignantes satisfont également à toutes les exigences d’alignement. Sun aligne donc tout en fonction de l’emplacement le plus restrictif.

Une autre tactique d'alignement de la mémoire peut récupérer une partie de cet espace .

  1. Si la classe ne contient aucune fonction virtuelle (et par conséquent, les instances de la classe n'ont pas de vptr), et si vous faites des hypothèses correctes sur la manière dont les données de membre de la classe sont mises en mémoire, faire ce que vous suggérez peut fonctionner (mais ne pas être portable).
  2. Oui, une autre façon (plus idiomatique mais pas beaucoup plus sûre ... vous devez toujours savoir comment la classe affiche ses données) serait d'utiliser le "nouvel opérateur de placement". et un constructeur par défaut.

Cela dépend de ce que vous entendez par "sûr". Chaque fois que vous convertissez une adresse mémoire en un point, vous contournez ainsi les fonctions de sécurité fournies par le compilateur et vous en assumez la responsabilité. Si, comme le laisse entendre Chris, vous faites une hypothèse incorrecte sur la structure de la mémoire ou sur les détails de la mise en œuvre du compilateur, vous obtiendrez des résultats inattendus et une portabilité lâche.

Puisque vous êtes préoccupé par la "sécurité" Dans ce style de programmation, il vaut probablement la peine d’étudier des méthodes portables et sécurisées, telles que des bibliothèques préexistantes ou l’écriture d’un constructeur ou d’un opérateur d’attribution à cette fin.

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