En train d'utiliser DELPHI pour développer des logiciels, nous sommes comme un groupe de vaches et de moutons heureux dans la prairie, profitant insouciante du soleil que nous apporte le langage Pascal Objet et des riches plantes aquatiques fournies par divers contrôles VCL. En regardant le ciel bleu sans limites, en regardant l'herbe verte luxuriante de la terre, qui penserait à la taille de l'univers et aux choses qui sont plus petites que les molécules et les atomes ? C'est une affaire de philosophes. A cette époque, le philosophe était assis au sommet d'une haute montagne, regardant les changements dans les nébuleuses de l'univers, fixant les insectes rampants sur le sol, se retournant soudainement, hochant la tête et souriant à notre groupe de pâturages. bovins et moutons. Il ramassa un morceau d'herbe, le tint doucement dans sa bouche, ferma les yeux et le goûta soigneusement. Je me demande quel était le goût de ce morceau d'herbe dans la bouche du philosophe ? Cependant, il avait toujours un sourire satisfait sur le visage.
Connaître et comprendre le monde atomique microscopique de DELPHI peut nous permettre de comprendre en profondeur la structure d'application macroscopique de DELPHI, développant ainsi notre logiciel dans un espace idéologique plus large. C'est comme si Newton avait découvert le mouvement des objets macroscopiques, mais qu'il était troublé parce qu'il ne parvenait pas à comprendre pourquoi les objets bougeaient ainsi. Au contraire, Einstein a vécu la vie heureuse de la relativité entre les lois des particules fondamentales et le mouvement des objets macroscopiques. !
Section 1 TObject Atome
Qu'est-ce que TObject ?
C'est le noyau de base de l'architecture du langage Pascal Objet et l'origine de divers contrôles VCL. Nous pouvons considérer TObject comme l'un des atomes qui composent une application DELPHI. Bien entendu, ils sont constitués de particules plus subtiles telles que des éléments de base de la syntaxe Pascal.
On dit que TObject est l'atome du programme DELPHI car TObject est pris en charge en interne par le compilateur DELPHI. Toutes les classes d'objets sont dérivées de TObject, même si vous ne spécifiez pas TObject comme classe ancêtre. TObject est défini dans l'unité System, qui fait partie du système. Au début de l'unité System.pas, il y a ce texte de commentaire :
{Constantes, types, procédures prédéfinies, }
{ et fonctions (telles que True, Integer ou }
{Writeln) n'a pas de déclarations réelles.}
{ Au lieu de cela, ils sont intégrés au compilateur }
{ et sont traités comme s'ils étaient déclarés }
{ au début de l'unité système }.
Cela signifie que cette unité contient des constantes, des types, des procédures et des fonctions prédéfinis (tels que : True, Integer ou Writeln). Ils ne sont pas réellement déclarés, mais sont intégrés par le compilateur et sont utilisés au début de la compilation. être une définition énoncée. Vous pouvez ajouter d'autres fichiers de programme source tels que Classes.pas ou Windows.pas à votre fichier de projet pour compiler et déboguer le code source, mais vous ne pouvez absolument pas ajouter le fichier de programme source System.pas à votre fichier de projet pour la compilation ! DELPHI signalera les erreurs de compilation pour les définitions en double de System !
Par conséquent, TObject est une définition fournie en interne par le compilateur. Pour ceux d'entre nous qui utilisent DELPHI pour développer des programmes, TObject est une chose atomique.
La définition de TObject dans l'unité System est la suivante :
TObject = classe
constructeur Créer ;
procédure Gratuit;
fonction de classe InitInstance(Instance : Pointeur) : TObject ;
procédure CleanupInstance ;
fonction ClassType : TClass ;
fonction de classe ClassName : ShortString ;
fonction de classe ClassNameIs (const Name : string) : Booléen ;
fonction de classe ClassParent : TClass ;
fonction de classe ClassInfo : pointeur ;
fonction de classe InstanceSize : Longint ;
fonction de classe InheritsFrom(AClass: TClass): Boolean;
fonction de classe MethodAddress (const Name : ShortString) : Pointeur ;
fonction de classe MethodName (Adresse : Pointeur) : ShortString ;
function FieldAddress (const Name : ShortString) : pointeur ;
fonction GetInterface (const IID : TGUID ; out Obj) : Booléen ;
fonction de classe GetInterfaceEntry(const IID : TGUID) : PInterfaceEntry ;
fonction de classe GetInterfaceTable : PInterfaceTable ;
fonction SafeCallException (ExceptObject: TObject;
ExceptAddr : Pointeur ): HResult ; virtuel ;
procédure AfterConstruction; virtuel;
procédure AvantDestruction ; virtuelle ;
procédure Dispatch(var Message);
procédure DefaultHandler(var Message);
fonction de classe NewInstance : TObject virtuel ;
procédure FreeInstance ; virtuelle ;
destructeur Détruire; virtuel;
fin;
Ensuite, nous frapperons progressivement à la porte des atomes de TObject pour voir quelle structure se trouve à l’intérieur.
Nous savons que TObject est la classe de base de tous les objets, alors qu'est-ce qu'un objet exactement ?
Tout objet dans DELPHI est un pointeur, qui indique l'espace occupé par l'objet en mémoire ! Bien que l'objet soit un pointeur, lorsque nous faisons référence aux membres de l'objet, nous n'avons pas besoin d'écrire le code MyObject^.GetName, mais nous pouvons uniquement écrire MyObject.GetName. Il s'agit d'une syntaxe étendue du langage Pascal Objet. pris en charge par le compilateur. Les amis qui utilisent C++ Builder sont très clairs sur la relation entre les objets et les pointeurs, car les objets dans C++ Builder doivent être définis comme des pointeurs. L'endroit pointé par le pointeur d'objet est l'espace objet où l'objet stocke les données. Analysons la structure des données de l'espace mémoire pointé par le pointeur d'objet.
Les 4 premiers octets de l'espace objet pointent vers la table d'adresses de méthode virtuelle (VMT – Virtual Method Table) de la classe d'objet. L'espace suivant est l'espace pour stocker les données membres de l'objet lui-même, et est stocké dans l'ordre total depuis les données membres de la classe ancêtre la plus primitive de l'objet jusqu'aux données membres de la classe d'objet, et dans l'ordre dans lequel les les données membres sont définies dans chaque niveau de classe.
La table de méthodes virtuelles (VMT) d'une classe contient les adresses de procédure des méthodes virtuelles de toutes les classes dérivées de la classe ancêtre d'origine de la classe. La méthode virtuelle d'une classe est une méthode déclarée avec le mot réservé virtual. La méthode virtuelle est le mécanisme de base pour réaliser le polymorphisme des objets. Bien que les méthodes dynamiques déclarées avec le mot réservé dynamique puissent également réaliser le polymorphisme d'objet, ces méthodes ne sont pas stockées dans la table d'adresses de méthode virtuelle (VMT). Il s'agit simplement d'une autre méthode fournie par Object Pascal qui peut économiser de l'espace de stockage de classe. mais au détriment de la vitesse d'appel.
Même si nous ne définissons nous-mêmes aucune méthode virtuelle de classe, l'objet de la classe a toujours un pointeur vers la table d'adresses de méthode virtuelle, mais la longueur de l'entrée d'adresse est nulle. Cependant, où sont stockées les méthodes virtuelles définies dans TObject, telles que Destroy, FreeInstance, etc. ? Il s'avère que leurs adresses de méthode sont stockées dans un espace décalé dans le sens négatif par rapport au pointeur VMT. En fait, l'espace de données décalé de 76 octets dans le sens négatif de la table VMT correspond à la structure de données système de la classe d'objets. Ces structures de données sont liées au compilateur et pourront être modifiées dans les futures versions de DELPHI.
Par conséquent, vous pouvez penser que VMT est une structure de données commençant à partir de l'espace d'adressage de décalage négatif. La zone de données de décalage négatif est la zone de données système de VMT, et les données de décalage positif de VMT sont la zone de données utilisateur (méthode virtuelle personnalisée). table d'adresses). Les fonctions et procédures liées aux informations de classe ou aux informations d'exécution d'objet définies dans TObject sont généralement liées aux données système de VMT.
Une donnée VMT représente une classe. En fait, VMT est une classe ! Dans Object Pascal, nous utilisons des identifiants tels que TObject, TComponent, etc. pour représenter les classes, qui sont implémentées en tant que données VMT respectives en interne dans DELPHI. Le type de classe défini avec la classe de mot réservé est en fait un pointeur vers les données VMT pertinentes.
Pour notre application, les données VMT sont des données statiques. Une fois que le compilateur a compilé notre application, ces informations sur les données ont été déterminées et initialisées. Les instructions de programme que nous écrivons peuvent accéder aux informations liées à VMT, obtenir des informations telles que la taille de l'objet, le nom de la classe ou les données d'attribut d'exécution, ou appeler des méthodes virtuelles ou lire le nom et l'adresse de la méthode, etc.
Lorsqu'un objet est généré, le système alloue un espace mémoire pour l'objet et associe l'objet à la classe appropriée. Par conséquent, les 4 premiers octets de l'espace de données alloué pour l'objet deviennent des pointeurs vers les données de classe VMT.
Jetons un coup d'œil à la façon dont les objets naissent et meurent. En regardant mon fils de trois ans sauter sur l'herbe, c'est précisément parce que j'ai été témoin du processus de naissance de la vie que je peux vraiment comprendre le sens et la grandeur de la vie. Seuls ceux qui ont vécu la mort comprendront et chériront davantage la vie. Alors, comprenons le processus de création et de mort des objets !
Nous savons tous que l’objet le plus simple peut être construit à l’aide de l’instruction suivante :
AnObject := TObject.Create;
Le compilateur implémente sa compilation comme :
En fonction du VMT correspondant à TObject, appelez le constructeur Create de TObject. Le constructeur Create appelle le processus ClassCreate du système, et le processus ClassCreate du système appelle la méthode virtuelle NewInstance via la classe VMT qui y est stockée. Le but de l'appel de la méthode NewInstance est d'établir l'espace d'instance de l'objet. Comme nous n'avons pas surchargé cette méthode, il s'agit de la NewInstance de la classe TObject. La méthode NewInstance de la classe TObjec appellera la procédure GetMem pour allouer de la mémoire à l'objet en fonction de la taille de l'instance d'objet (InstanceSize) initialisée par le compilateur dans la table VMT, puis appellera la méthode InitInstance pour initialiser l'espace alloué. La méthode InitInstance initialise d'abord les 4 premiers octets de l'espace objet avec un pointeur vers le VMT correspondant à la classe d'objet, puis efface l'espace restant. Après avoir établi l’instance d’objet, une méthode virtuelle AfterConstruction est également appelée. Enfin, enregistrez le pointeur d'adresse des données d'instance d'objet dans la variable AnObject, et de cette façon, l'objet AnObject est né.
De même, un objet peut être détruit à l'aide de l'instruction suivante :
AnObject.Destroy ;
Le destructeur de TObject, Destroy, est déclaré comme une méthode virtuelle, qui est également l'une des méthodes virtuelles inhérentes au système. La méthode Destory appelle d’abord la méthode virtuelle BeforeDestruction, puis appelle le processus ClassDestroy du système. Le processus ClassDestory appelle la méthode virtuelle FreeInstance via la classe VMT, et la méthode FreeInstance appelle le processus FreeMem pour libérer l'espace mémoire de l'objet. C'est ainsi qu'un objet disparaît du système.
Le processus de destruction des objets est plus simple que le processus de construction des objets, tout comme la naissance de la vie est un long processus de gestation, mais la mort est relativement de courte durée. Cela semble être une règle inévitable.
Lors du processus de construction et de destruction de l'objet, deux fonctions virtuelles, NewInstance et FreeInstance, sont appelées pour créer et libérer l'espace mémoire de l'instance d'objet. La raison pour laquelle ces deux fonctions sont déclarées comme fonctions virtuelles est de permettre aux utilisateurs de disposer d'une marge d'expansion lors de l'écriture de classes d'objets spéciales qui nécessitent que les utilisateurs gèrent leur propre mémoire (comme dans certains programmes de contrôle industriel spéciaux).
Déclarer AfterConstruction et BeforeDestruction comme fonctions virtuelles, c'est également donner à la classe dérivée dans le futur la possibilité de laisser l'objet nouvellement né respirer la première bouffée d'air frais après avoir généré l'objet, et de permettre à l'objet de terminer les conséquences avant que l'objet ne meure. . Tout cela a du sens. En fait, l'événement OnCreate et l'événement OnDestroy de l'objet TForm et de l'objet TDataModule sont déclenchés respectivement dans les deux processus de fonction virtuelle de surcharge TForm et TDataModule.
De plus, TObjec fournit également une méthode Free, qui n'est pas une méthode virtuelle. Elle est spécialement prévue pour libérer l'objet en toute sécurité lorsqu'il n'est pas clair si l'objet est vide (nil). En fait, si vous ne parvenez pas à déterminer si l'objet est vide, il existe un problème de logique de programme peu claire. Cependant, personne n’est parfait et ne peut faire d’erreurs. C’est aussi une bonne chose d’utiliser Free pour éviter les erreurs accidentelles. Cependant, l’écriture de programmes corrects ne peut pas s’appuyer uniquement sur de telles solutions. Le premier objectif de la programmation devrait être de garantir l’exactitude logique du programme !
Les amis intéressés peuvent lire le code original de l'unité système, où une grande quantité de code est écrite en langage assembleur. Les amis prudents peuvent constater que le constructeur Create et le destructeur Destory de TObject n'ont écrit aucun code. En fait, via la fenêtre Debug CPU dans l'état de débogage, le code d'assemblage de Create et Destory peut être clairement reflété. Parce que les maîtres qui ont créé DELPHI ne voulaient pas fournir aux utilisateurs trop de choses compliquées. Ils voulaient que les utilisateurs écrivent des applications basées sur des concepts simples et cachent le travail complexe à entreprendre à l'intérieur du système. Par conséquent, lors de la publication de l'unité System.pas, les codes de ces deux fonctions sont spécialement supprimés pour faire croire aux utilisateurs que TObject est la source de toutes choses et que les classes dérivées de l'utilisateur partent complètement du néant. Bien que la lecture de ces codes les plus essentiels de DELPHI nécessite une petite quantité de connaissances en langage assembleur, la lecture de ces codes peut nous donner une compréhension plus approfondie de l'origine et du développement du monde DELPHI. Même si vous ne comprenez pas grand chose, être capable au moins de comprendre certaines choses de base nous sera d'une grande aide pour écrire des programmes DELPHI.
Section 2 TClass Atome
Dans l'unité System.pas, TClass est défini comme ceci :
TClass = classe de TObject ;
Cela signifie que TClass est la classe de TObject. Parce que TObject lui-même est une classe, TClass est ce qu'on appelle la classe des classes.
Conceptuellement, TClass est un type de classe, c'est-à-dire une classe. Cependant, nous savons qu'une classe de DELPHI représente une donnée VMT. La classe peut donc être considérée comme le type défini pour la donnée VMT. En fait, il s’agit d’un type pointeur pointant vers les données VMT !
Dans le langage C++ traditionnel précédent, le type d’une classe ne pouvait pas être défini. Une fois l'objet compilé, il est corrigé, les informations structurelles de la classe ont été converties en code machine absolu et les informations complètes de la classe n'existeront pas dans la mémoire. Certains langages orientés objet de niveau supérieur peuvent prendre en charge l'accès dynamique et l'invocation d'informations de classe, mais ils nécessitent souvent un mécanisme d'interprétation interne complexe et davantage de ressources système. Le langage Pascal Objet de DELPHI absorbe certaines des excellentes fonctionnalités des langages orientés objet de haut niveau, tout en conservant l'avantage traditionnel de compiler directement les programmes en code machine, ce qui résout parfaitement les problèmes de fonctions avancées et d'efficacité des programmes.
C'est précisément parce que DELPHI conserve des informations complètes sur les classes dans l'application qu'il peut fournir des fonctions avancées orientées objet telles que la conversion et l'identification des classes au moment de l'exécution, dans lesquelles les données VMT de la classe jouent un rôle central clé. Les amis intéressés peuvent lire les deux processus d'assemblage d'AsClass et IsClass dans l'unité Système. Ce sont les codes d'implémentation des opérateurs as et is pour approfondir leur compréhension des classes et des données VMT.
Le monde atomique de DELPHI (2)
Mots-clés : contrôles Delphi divers
Section 2 TClass Atome
Dans l'unité System.pas, TClass est défini comme ceci :
TClass = classe de TObject ;
Cela signifie que TClass est la classe de TObject. Parce que TObject lui-même est une classe, TClass est ce qu'on appelle la classe des classes.
Conceptuellement, TClass est un type de classe, c'est-à-dire une classe. Cependant, nous savons qu'une classe de DELPHI représente une donnée VMT. La classe peut donc être considérée comme le type défini pour la donnée VMT. En fait, il s’agit d’un type pointeur pointant vers les données VMT !
Dans le langage C++ traditionnel précédent, le type d’une classe ne pouvait pas être défini. Une fois l'objet compilé, il est corrigé, les informations structurelles de la classe ont été converties en code machine absolu et les informations complètes de la classe n'existeront pas dans la mémoire. Certains langages orientés objet de niveau supérieur peuvent prendre en charge l'accès dynamique et l'invocation d'informations de classe, mais ils nécessitent souvent un mécanisme d'interprétation interne complexe et davantage de ressources système. Le langage Pascal Objet de DELPHI absorbe certaines des excellentes fonctionnalités des langages orientés objet de haut niveau, tout en conservant l'avantage traditionnel de compiler directement les programmes en code machine, ce qui résout parfaitement les problèmes de fonctions avancées et d'efficacité des programmes.
C'est précisément parce que DELPHI conserve des informations complètes sur les classes dans l'application qu'il peut fournir des fonctions avancées orientées objet telles que la conversion et l'identification des classes au moment de l'exécution, dans lesquelles les données VMT de la classe jouent un rôle central clé. Les amis intéressés peuvent lire les deux processus d'assemblage d'AsClass et IsClass dans l'unité Système. Ce sont les codes d'implémentation des opérateurs as et is pour approfondir leur compréhension des classes et des données VMT.
Avec le type de classe, vous pouvez utiliser la classe comme variable. Une variable de classe peut être comprise comme un objet spécial et vous pouvez accéder aux méthodes d'une variable de classe comme un objet. Par exemple : Jetons un coup d'œil au fragment de programme suivant :
taper
TSampleClass = classe de TSampleObject ;
TSampleObject = classe (TObject)
publique
constructeur Créer ;
destructeur Détruire ;
fonction de classe GetSampleObjectCount:Integer ;
procédure GetObjectIndex:Integer;
fin;
var
aSampleClass : TSampleClass ;
aClasse : TClass ;
Dans ce code, nous définissons une classe TSampleObject et son type de classe associé TSampleClass, ainsi que deux variables de classe aSampleClass et aClass. De plus, nous avons également défini un constructeur, un destructeur, une méthode de classe GetSampleObjectCount et une méthode objet GetObjectIndex pour la classe TSampleObject.
Tout d'abord, comprenons la signification des variables de classe aSampleClass et aClass.
Évidemment, vous pouvez traiter TSampleObject et TObject comme des valeurs constantes et les affecter à des variables aClass, tout comme attribuer 123 valeurs constantes à la variable entière i. Par conséquent, la relation entre les types de classe, les classes et les variables de classe est la relation entre les types, les constantes et les variables, mais au niveau de la classe plutôt qu'au niveau de l'objet. Bien sûr, il n'est pas légal d'attribuer directement TObject à aSampleClass, car aSampleClass est une variable de classe de la classe TSampleObject dérivée de TObject, et TObject ne contient pas toutes les définitions compatibles avec le type TSampleClass. Au contraire, il est légal d'affecter TSampleObject à une variable Class, car TSampleObject est une classe dérivée de TObject et est compatible avec le type TClass. Ceci est exactement similaire à la relation d'affectation et de correspondance de type des variables d'objet.
Voyons ensuite ce que sont les méthodes de classe.
La méthode dite de classe fait référence à la méthode appelée au niveau de la classe, comme la méthode GetSampleObjectCount définie ci-dessus, qui est une méthode déclarée avec le mot réservé class. Les méthodes de classe sont différentes des méthodes d'objet appelées au niveau de l'objet. Les méthodes d'objet nous sont déjà familières, et les méthodes de classe sont toujours utilisées au niveau de l'accès et du contrôle des caractéristiques communes de tous les objets de classe et de la gestion centralisée des objets. Dans la définition de TObject, on peut trouver un grand nombre de méthodes de classe, telles que ClassName, ClassInfo, NewInstance, etc. Parmi eux, NewInstance est également défini comme virtuel, c'est-à-dire une méthode de classe virtuelle. Cela signifie que vous pouvez réécrire la méthode d'implémentation de NewInstance dans une sous-classe dérivée pour construire des instances d'objet de cette classe d'une manière spéciale.
Vous pouvez également utiliser l'identifiant self dans les méthodes de classe, mais sa signification est différente de celle de self dans les méthodes objet. Le self dans la méthode de classe représente sa propre classe, c'est-à-dire le pointeur vers le VMT, tandis que le self dans la méthode objet représente l'objet lui-même, c'est-à-dire le pointeur vers l'espace de données de l'objet. Bien que les méthodes de classe ne puissent être utilisées qu’au niveau de la classe, vous pouvez toujours appeler des méthodes de classe via un objet. Par exemple, la méthode de classe ClassName de l'objet TObject peut être appelée via l'instruction aObject.ClassName, car les 4 premiers octets de l'espace de données de l'objet pointés par le pointeur d'objet sont des pointeurs vers la classe VMT. Au contraire, vous ne pouvez pas appeler de méthodes objet au niveau de la classe et les instructions telles que TObject.Free doivent être illégales.
Il est à noter que le constructeur est une méthode de classe et le destructeur est une méthode objet !
Quoi? Les constructeurs sont des méthodes de classe et les destructeurs sont des méthodes d'objet ! Y a-t-il eu une erreur ?
Vous voyez, lorsque vous créez un objet, vous utilisez clairement une instruction similaire à la suivante :
aObject := TObject.Create;
Il appelle clairement la méthode Create de la classe TObject. Lors de la suppression d'un objet, utilisez l'instruction suivante :
aObject.Destroy;
Même si vous utilisez la méthode Free pour libérer l'objet, la méthode Destroy de l'objet est appelée indirectement.
La raison est très simple. Avant que l'objet ne soit construit, l'objet n'existe pas encore, seule la classe existe. Vous ne pouvez utiliser que des méthodes de classe pour créer des objets. Au contraire, la suppression d'un objet doit supprimer l'objet existant, c'est l'objet qui est libéré, pas la classe.
Enfin, abordons la question des constructeurs fictifs.
Dans le langage C++ traditionnel, des destructeurs virtuels peuvent être implémentés, mais l'implémentation de constructeurs virtuels est un problème difficile. Parce que, dans le langage C++ traditionnel, il n’existe pas de types de classes. Des instances d'objets globaux existent dans l'espace de données global au moment de la compilation, et les objets locaux des fonctions sont également des instances mappées dans l'espace de pile au moment de la compilation. Même les objets créés dynamiquement sont placés dans la structure de classe fixe à l'aide de l'opérateur new. dans l'espace du tas, et le constructeur n'est qu'une méthode objet qui initialise l'instance d'objet générée. Il n'existe pas de véritables méthodes de classe dans le langage C++ traditionnel. Même si des méthodes dites statiques basées sur des classes peuvent être définies, elles sont finalement implémentées en tant que fonction globale spéciale, sans parler des méthodes de classe virtuelle qui ne peuvent cibler que des objets spécifiques. instances. Par conséquent, le langage C++ traditionnel estime qu'avant qu'une instance d'objet spécifique ne soit générée, il est impossible de construire l'objet lui-même en fonction de l'objet à générer. C’est en effet impossible, car cela créerait un paradoxe logique contradictoire !
Cependant, c'est précisément grâce aux concepts clés d'informations sur les types de classes dynamiques, de méthodes de classes véritablement virtuelles et de constructeurs implémentés sur la base de classes dans DELPHI que les constructeurs virtuels peuvent être implémentés. Les objets sont produits par les classes. L'objet est comme un bébé qui grandit, et la classe est sa mère. Le bébé lui-même ne sait pas quel genre de personne il deviendra dans le futur, mais les mères utilisent leurs propres méthodes éducatives pour éduquer différents enfants. . Les gens, les principes sont les mêmes.
C'est dans la définition de la classe TComponent que le constructeur Create est défini comme virtuel afin que différents types de contrôles puissent implémenter leurs propres méthodes de construction. C'est la grandeur des concepts comme les classes créées par TClass, et aussi la grandeur de DELPHI.
.................................................................. ..
Chapitre 3 La vue du temps et de l'espace dans WIN32
Mon vieux père a regardé son petit-fils jouer avec des jouets par terre, puis m'a dit : « Cet enfant est comme toi quand tu étais enfant. Il aime démonter les choses et ne s'arrête qu'après les avoir vus jusqu'au bout. " En repensant à mon enfance, je démontais souvent des petites voitures, des petits réveils, des boîtes à musique, etc., et j'étais souvent grondé par ma mère.
La première fois que j'ai compris les principes de base de l'informatique, c'était avec une boîte à musique que j'avais démontée. C'était dans une bande dessinée quand j'étais au lycée. Un vieil homme à la barbe blanche expliquait la théorie des machines intelligentes, et un oncle moustachu parlait d'ordinateurs et de boîtes à musique. Ils ont dit que l'unité centrale d'un ordinateur est la rangée d'anches musicales utilisée pour la prononciation dans la boîte à musique, et que le programme informatique est constitué des bosses densément emballées sur le petit cylindre de la boîte à musique. La rotation du petit cylindre est équivalente. à la rotation de l'unité centrale. Le mouvement naturel du pointeur d'instruction, tandis que les bosses représentant la musique sur le petit cylindre contrôlent la vibration de l'anche musicale pour produire des instructions équivalentes à l'exécution du programme par le processeur central. La boîte à musique émet une belle mélodie, qui est jouée selon la partition musicale gravée sur le petit cylindre par l'artisan. L'ordinateur effectue un traitement complexe basé sur le programme préprogrammé par le programmeur. Après mes études universitaires, j’ai appris que le vieil homme à la barbe blanche était le géant scientifique Turing. Sa théorie des automates finis favorisait le développement de toute la révolution de l’information, et l’oncle à la moustache était le père des ordinateurs, von Neumann. L'architecture informatique reste la principale structure architecturale des ordinateurs. La boîte à musique n'a pas été démontée en vain, maman peut se rassurer.
Ce n'est qu'avec une compréhension simple et profonde que nous pouvons créer des créations profondes et concises.
Dans ce chapitre, nous aborderons les concepts de base liés à notre programmation dans le système d'exploitation Windows 32 bits et établirons la vision correcte du temps et de l'espace dans WIN32. J'espère qu'après avoir lu ce chapitre, nous pourrons avoir une compréhension plus approfondie des programmes, des processus et des threads, comprendre les principes des fichiers exécutables, des bibliothèques de liens dynamiques et des packages d'exécution, et voir clairement la vérité sur les données globales, les données locales et les paramètres en mémoire. .
Section 1 Comprendre le processus
Pour des raisons historiques, Windows provient du DOS. À l’ère DOS, nous n’avions toujours que la notion de programme, mais pas la notion de processus. À cette époque, seuls les systèmes d'exploitation classiques, tels qu'UNIX et VMS, avaient le concept de processus, et multi-processus signifiait des mini-ordinateurs, des terminaux et des utilisateurs multiples, ce qui signifiait également de l'argent. La plupart du temps, je ne pouvais utiliser que des micro-ordinateurs et des systèmes DOS relativement bon marché. Ce n'est que lorsque j'ai étudié les systèmes d'exploitation que j'ai commencé à entrer en contact avec les processus et les mini-ordinateurs.
Ce n'était qu'après Windows 3. Dans le passé, sous DOS, un seul programme pouvait être exécuté en même temps, mais sous Windows, plusieurs programmes pouvaient être exécutés en même temps. Lors de l'exécution d'un programme sous DOS, le même programme ne peut pas être exécuté en même temps, mais sous Windows, plus de deux copies du même programme peuvent être exécutées en même temps, et chaque copie en cours d'exécution du programme est un processus. Pour être plus précis, chaque exécution d'un programme génère une tâche, et chaque tâche est un processus.
Lorsque les programmes et les processus sont compris ensemble, le mot programme peut être considéré comme faisant référence à des éléments statiques. Un programme typique est un code statique et des données composés d'un fichier EXE ou d'un fichier EXE plus plusieurs fichiers DLL. Un processus est une exécution d'un programme, qui est du code et des données changeant dynamiquement qui s'exécutent dynamiquement en mémoire. Lorsqu'un programme statique doit être exécuté, le système d'exploitation fournira un certain espace mémoire pour cette opération, transférera le code du programme statique et les données dans ces espaces mémoire, et repositionnera et mappera le code du programme et les données dans cet espace. exécuté à l’intérieur, créant ainsi un processus dynamique.
Deux copies du même programme exécutées en même temps signifient qu'il existe deux espaces de processus dans la mémoire système, mais leurs fonctions de programme sont les mêmes, mais ils sont dans des états différents changeant dynamiquement.
En termes de durée d'exécution du processus, chaque processus est exécuté en même temps. Le terme professionnel est appelé exécution parallèle ou exécution concurrente. Mais c'est principalement le sentiment superficiel que nous donne le système d'exploitation. En fait, chaque processus est exécuté en temps partagé, c'est-à-dire que chaque processus occupe à tour de rôle le temps CPU pour exécuter les instructions du programme du processus. Pour un CPU, seules les instructions d'un processus sont exécutées en même temps. Le système d'exploitation est le manipulateur derrière le fonctionnement du processus planifié. Il enregistre et change constamment l'état actuel de chaque processus exécuté dans le CPU, de sorte que chaque processus planifié pense qu'il s'exécute complètement et en continu. La planification des processus en temps partagé étant très rapide, cela nous donne l’impression que les processus s’exécutent tous en même temps. En fait, un véritable fonctionnement simultané n’est possible que dans un environnement matériel multi-CPU. Lorsque nous parlerons des threads plus tard, nous constaterons que ce sont les threads qui pilotent réellement le processus et, plus important encore, ils fournissent de l'espace de processus.
En termes d'espace occupé par le processus, chaque espace de processus est relativement indépendant et chaque processus s'exécute dans son propre espace indépendant. Un programme comprend à la fois un espace de code et un espace de données. Le code et les données occupent l'espace de processus. Windows alloue de la mémoire réelle pour l'espace de données requis par chaque processus et utilise généralement des méthodes de partage pour l'espace de code, mappant un code d'un programme à plusieurs processus du programme. Cela signifie que si un programme contient 100 Ko de code et nécessite 100 Ko d'espace de données, ce qui signifie qu'un total de 200 Ko d'espace de processus est requis, le système d'exploitation allouera 200 Ko d'espace de processus lors de la première exécution du programme et 200 Ko d'espace de processus. l'espace sera alloué lors de la deuxième exécution du programme. Lorsqu'un processus est démarré, le système d'exploitation n'alloue que 100 Ko d'espace de données, tandis que l'espace de code partage l'espace du processus précédent.
Ce qui précède est la vue temporelle et spatiale de base du processus dans le système d'exploitation Windows. En fait, il existe une grande différence dans la vue temporelle et spatiale du processus entre les systèmes d'exploitation 16 bits et 32 bits de Windows.
En termes de temps, la gestion des processus des systèmes d'exploitation Windows 16 bits, tels que Windows 3.x, est très simple. Il s'agit en fait d'un système d'exploitation de gestion multitâche. De plus, la planification des tâches du système d'exploitation est passive. Si une tâche n'abandonne pas le traitement du message, le système d'exploitation doit attendre. En raison des failles dans la gestion des processus du système Windows 16 bits, lorsqu'un processus est en cours d'exécution, il occupe entièrement les ressources du processeur. À cette époque, pour que Windows 16 bits ait la possibilité de planifier d'autres tâches, Microsoft félicitait les développeurs d'applications Windows d'être des programmeurs à l'esprit large, de sorte qu'ils étaient prêts à écrire quelques lignes de code supplémentaires pour offrir le système opérateur. Au contraire, les systèmes d'exploitation WIN32, tels que Windows 95 et NT, disposent de véritables capacités de système d'exploitation multi-processus et multi-tâches. Le processus dans WIN32 est entièrement planifié par le système d'exploitation. Une fois la tranche de temps du processus en cours d'exécution terminée, le système d'exploitation passera activement au processus suivant, que le processus soit ou non en train de traiter des données. À proprement parler, le système d'exploitation Windows 16 bits ne peut pas être considéré comme un système d'exploitation complet, mais le système d'exploitation Win32 32 bits est le véritable système d'exploitation. Bien sûr, Microsoft ne dira pas que Win32 compense les lacunes des fenêtres 16 bits, mais affirme que Win32 implémente une technologie de pointe appelée "multitâche préemptive", qui est une méthode commerciale.
Du point de vue de l'espace, bien que l'espace de processus dans le système d'exploitation Windows 16 bits soit relativement indépendant, les processus peuvent facilement accéder à l'espace de données de l'autre. Étant donné que ces processus sont en fait des segments de données différents dans le même espace physique, et les opérations d'adresse inappropriées peuvent facilement provoquer une lecture et une écriture d'espace incorrectes et écraser le système d'exploitation. Cependant, dans le système d'exploitation Win32, chaque espace de processus est complètement indépendant. Win32 fournit à chaque processus un espace d'adressage virtuel et continu jusqu'à 4G. Le soi-disant espace d'adressage continu signifie que chaque processus a un espace d'adressage de 0000000000 à $ FFFFFFFF, plutôt que l'espace segmenté des fenêtres 16 bits. Dans Win32, vous n'avez pas à vous soucier de vos opérations de lecture et d'écriture affectant involontairement les données dans d'autres espaces de processus, et vous n'avez pas à vous soucier des autres processus pour harceler votre travail. En même temps, l'espace virtuel 4G continu fourni par Win32 pour votre processus est la mémoire physique mappée par le système d'exploitation avec le support du matériel. .
Section 2 Space de processus
Lorsque nous utilisons Delphi pour écrire des applications Win32, nous nous soucions rarement du monde interne du processus lorsqu'il s'exécute. Parce que Win32 fournit une 4G d'espace de processus virtuel continu pour notre processus, la plus grande application au monde n'en utilise actuellement qu'une partie. Il semble que l'espace de processus soit illimité, mais l'espace de processus 4G est virtuel et la mémoire réelle de votre machine peut être loin de cela. Bien que le processus ait un espace aussi vaste, certains programmes d'algorithmes complexes ne seront toujours pas en mesure de s'exécuter en raison de Stack Overflow, en particulier des programmes contenant un grand nombre d'algorithmes récursifs.
Par conséquent, une compréhension approfondie de la structure de l'espace de processus 4G, sa relation avec la mémoire physique, etc. nous aidera à comprendre le monde de l'espace-temps de Win32 plus clairement, afin que nous puissions utiliser les méthodes correctes dans le travail de développement réel . Vieille du monde et méthodologie pour résoudre divers problèmes difficiles.
Ensuite, nous utiliserons une expérience simple pour comprendre le monde interne de l'espace de processus de Win32. Cela peut nécessiter une certaine connaissance des registres de coupe et du langage d'assemblage, mais j'ai essayé de l'expliquer dans un langage simple.
Lorsque Delphi est démarré, un projet Project1 sera généré automatiquement et nous commencerons par lui. Définissez un point d'arrêt n'importe où dans le programme original de Project1.dpr, par exemple, définissez un point d'arrêt à la phrase de début. Ensuite, exécutez le programme et il s'arrête automatiquement lorsqu'il atteindra le point d'arrêt. À l'heure actuelle, nous pouvons ouvrir la fenêtre CPU dans l'outil de débogage pour observer la structure interne de l'espace de processus.
Le registre actuel du pointeur EIP est arrêté à 0043e4b8. L'espace de processus, qui occupe 0000000000 à un espace d'adressage assez petit pour $ ffffffff.
Dans la boîte de commande dans la fenêtre CPU, vous pouvez consulter le contenu de l'espace de processus. Lorsque vous consultez le contenu de l'espace inférieur à 00400000, vous trouverez une série de points d'interrogation "????" dans le contenu inférieur à 00400000. Si vous regardez la valeur hexadécimale de la variable globale Hinstance pour le moment, vous constaterez qu'il est également 00400000 $. Bien que Hinstance reflète la poignée de l'instance de processus, en fait, c'est la valeur d'adresse de départ lorsque le programme est chargé en mémoire, également dans des fenêtres 16 bits. Par conséquent, nous pouvons penser que le programme du processus est chargé à partir de 00400000 $, c'est-à-dire que l'espace à partir de 4 m dans l'espace virtuel 4G est l'espace où le programme est chargé.
À partir de 00400000 $ et avant 0044d000 $, il s'agit principalement de l'espace d'adresse du code du programme et des données globales. Dans la boîte de pile dans la fenêtre CPU, vous pouvez afficher l'adresse de la pile actuelle. De même, vous constaterez que l'espace d'adressage de pile actuel est de 0067b000 à 00680000 $, avec une longueur de 5000 $. En fait, la taille minimale de l'espace de pile du processus est de 5000 $, qui est obtenue sur la base de la valeur de taille de pile Min définie dans la page de liaison de ProjectOptions lors de la compilation du programme Delphi, plus 1000 $. La pile passe de l'adresse haut de gamme vers le bas. processus d'espace. Lors de la compilation d'un programme Delphi, vous pouvez contrôler l'espace de pile maximal qui peut être augmenté en définissant la valeur de la taille de la pile maximale dans la page de liaison dans ProjectOptions. En particulier dans les programmes contenant des relations d'appel de sous-programme en profondeur ou utilisent des algorithmes récursifs, la valeur de la taille de la pile maximale doit être définie raisonnablement. Étant donné que l'appel d'un sous-programme nécessite un espace de pile, et une fois la pile épuisée, le système lancera une erreur de "débordement de pile".
Il semble que l'espace de processus après l'espace de pile soit l'espace libre. En fait, ce n'est pas le cas. Il semble que le processus ne puisse vraiment posséder qu'un espace 2G. En fait, l'espace qu'un processus peut vraiment posséder n'est même pas 2G, car l'espace 4 m de 00000000 à 00400000 $ est également une zone restreinte.
Mais quoi qu'il arrive, les adresses que notre processus peut utiliser sont encore très larges. Surtout après l'espace de pile et entre 80 000 000 $, c'est le principal champ de bataille de l'espace de processus. L'espace mémoire alloué par le processus du système sera mappé dans cet espace, la bibliothèque de liens dynamiques chargée par le processus sera mappée sur cet espace, l'espace de pile de threads du nouveau thread sera également mappé à cet espace, presque tout Les opérations impliquant une allocation de mémoire seront toutes mappées à cet espace. Veuillez noter que le mappage mentionné ici signifie la correspondance entre la mémoire réelle et cet espace virtuel. ???? ".