Je me demande ce que vous essayez d'accomplir. Si votre processus est déterministe, le schéma d'allocation/désallocation doit être le même.
La seule différence possible pourrait être l'adresse retournée par malloc
. Mais vous ne devriez probablement pas en dépendre (le moyen le plus simple étant de ne pas utiliser de pointeurs comme carte clé ou autre structure de données). Et même dans ce cas, il ne devrait y avoir de différence que si l'allocation ne se fait pas via sbrk
(la glibc utilise mmap
anonyme pour les grandes allocations), ou si vous utilisez mmap
(car par défaut l'adresse est sélectionnée par le noyau).
Si vous voulez vraiment avoir exactement la même adresse, une option consiste à avoir un grand tampon statique et à écrire un répartiteur personnalisé qui utilise la mémoire de ce tampon. Cela a l'inconvénient de vous obliger à connaître à l'avance la quantité maximale de mémoire dont vous aurez besoin. Dans un exécutable non-PIE (gcc -fno-pie -no-pie
), un tampon statique aura la même adresse à chaque fois. Pour un exécutable PIE, vous pouvez désactiver la randomisation de la disposition de l'espace d'adressage du noyau pour le chargement des programmes. Dans une bibliothèque partagée, la désactivation de l'ASLR et l'exécution du même programme deux fois devraient conduire aux mêmes choix par l'éditeur de liens dynamique pour savoir où mapper les bibliothèques.
Si vous ne connaissez pas à l'avance la taille maximale de la mémoire que vous souhaitez utiliser, ou si vous ne souhaitez pas recompiler à chaque fois que cette taille augmente, vous pouvez également utiliser mmap
pour mapper un grand tampon anonyme à une adresse fixe. Passez simplement la taille du tampon et l'adresse à utiliser comme paramètre à votre processus et utilisez la mémoire renvoyée pour implémenter votre propre malloc
dessus.
static void* malloc_buffer = NULL;
static size_t malloc_buffer_len = 0;
void* malloc(size_t size) {
// Use malloc_buffer & malloc_buffer_len to implement your
// own allocator. If you don't read uninitialized memory,
// it can be deterministic.
return memory;
}
int main(int argc, char** argv) {
size_t buf_size = 0;
uintptr_t buf_addr = 0;
for (int i = 0; i < argv; ++i) {
if (strcmp(argv[i], "--malloc-size") == 0) {
buf_size = atoi(argv[++i]);
}
if (strcmp(argv[i], "--malloc-addr") == 0) {
buf_addr = atoi(argv[++i]);
}
}
malloc_buffer = mmap((void*)buf_addr, buf_size, PROT_WRITE|PROT_READ,
MAP_FIXED|MAP_PRIVATE, 0, 0);
// editor's note: omit MAP_FIXED since you're checking the result anyway
if (malloc_buffer == MAP_FAILED || malloc_buffer != (void*)but_addr) {
// Could not get requested memory block, fail.
exit(1);
}
malloc_size = buf_size;
}
En utilisant MAP_FIXED
, nous disons au noyau de remplacer tous les mappages existants qui se chevauchent avec ce nouveau à buf_addr
.
(NDLR :MAP_FIXED
n'est probablement pas ce que vous voulez . Spécifier buf_addr
comme indice au lieu de NULL
demande déjà cette adresse si possible. Avec MAP_FIXED
, mmap
renverra soit une erreur, soit l'adresse que vous lui avez donnée. Le malloc_buffer != (void*)but_addr
check a du sens pour les non-FIXED
cas, qui ne remplacera pas un mappage existant de votre code ou une bibliothèque partagée ou quoi que ce soit d'autre. Linux 4.17 introduit MAP_FIXED_NOREPLACE
que vous pouvez utiliser pour que mmap renvoie une erreur au lieu de la mémoire à la mauvaise adresse que vous ne voulez pas utiliser. Mais laissez toujours l'enregistrement pour que votre code fonctionne sur les noyaux plus anciens.)
Si vous utilisez ce bloc pour implémenter votre propre malloc et n'utilisez pas d'autre opération non déterministe dans votre code, vous pouvez avoir un contrôle total sur les valeurs du pointeur.
Cela suppose que votre modèle d'utilisation de malloc / free est déterministe. Et que vous n'utilisez pas de bibliothèques non déterministes.
Cependant, je pense qu'une solution plus simple consiste à garder vos algorithmes déterministes et à ne pas dépendre des adresses. C'est possible. J'ai travaillé sur un projet à grande échelle où plusieurs ordinateurs devaient mettre à jour l'état de manière déterministe (afin que chaque programme ait le même état, tout en ne transmettant que des entrées). Si vous n'utilisez pas de pointeur pour autre chose que le référencement d'objets (le plus important est de ne jamais utiliser la valeur du pointeur pour quoi que ce soit, pas comme un hachage, pas comme une clé dans une carte, ...), alors votre état restera déterministe .
À moins que ce que vous vouliez faire soit de pouvoir prendre un instantané de toute la mémoire du processus et de faire un diff binaire pour repérer la divergence. Je pense que c'est une mauvaise idée, car comment saurez-vous que les deux ont atteint le même point dans leur calcul ? Il est beaucoup plus facile de comparer la sortie, ou de faire en sorte que le processus puisse calculer un hachage de l'état et l'utiliser pour vérifier qu'ils sont synchronisés car vous pouvez contrôler quand cela est fait (et donc cela devient également déterministe, sinon votre mesure est non déterministe).
Ce qui n'est pas déterministe n'est pas seulement malloc
mais mmap (l'appel système de base pour obtenir plus d'espace mémoire ; ce n'est pas une fonction, c'est un appel système, il est donc élémentaire ou atomique du point de vue de l'application ; vous ne pouvez donc pas le réécrire dans l'application) à cause de la randomisation de la disposition de l'espace d'adressage sous Linux.
Vous pouvez le désactiver avec
echo 0 > /proc/sys/kernel/randomize_va_space
en tant que root, ou via sysctl.
Si vous ne désactivez pas la randomisation de la disposition de l'espace d'adressage, vous êtes bloqué.
Et vous avez posé une question similaire précédemment, où j'ai expliqué que votre malloc
-s ne sera pas toujours déterministe.
Je pense toujours que pour certaines applications pratiques, malloc
ne peut pas être déterministe. Imaginez par exemple un programme ayant une table de hachage codée par le pid
-s des processus enfants qu'il lance. La collision dans cette table ne sera pas la même dans tous vos processus, etc.
Donc je crois que vous ne réussirez pas à faire malloc
déterministe dans votre sens, quoi que vous essayiez (à moins que vous ne vous restreigniez à une classe très étroite d'applications au point de contrôle, si étroite que votre logiciel ne sera pas très utile).
En termes simples, comme d'autres l'ont dit :si l'exécution des instructions de votre programme est déterministe, alors la mémoire est renvoyée par malloc()
sera déterministe. Cela suppose que l'implémentation de votre système n'a pas d'appel à random()
Ou quelque chose à cet effet. Si vous n'êtes pas sûr, lisez le code ou la documentation du malloc
de votre système .
C'est à l'exception possible de l'ASLR, comme d'autres l'ont également déclaré. Si vous n'avez pas les privilèges root, vous pouvez le désactiver par processus via le personality(2)
syscall et le paramètre ADDR_NO_RANDOMIZE. Voir ici pour plus d'informations sur les personnalités.
Edit :Je devrais également dire, si vous n'êtes pas au courant :ce que vous faites s'appelle bisimulation et est une technique bien étudiée. Si vous ne connaissiez pas la terminologie, il pourrait être utile d'avoir ce mot-clé pour la recherche.