En el proceso de utilizar DELPHI para desarrollar software, somos como un grupo de vacas y ovejas felices en la pradera, disfrutando despreocupadamente de la luz del sol que nos brinda el lenguaje Object Pascal y las ricas plantas acuáticas proporcionadas por varios controles VCL. Mirando hacia el infinito cielo azul, mirando hacia la exuberante hierba verde de la tierra, ¿quién pensaría en lo grande que es el universo y qué cosas son más pequeñas que las moléculas y los átomos? Ésa es una cuestión de filósofos. En ese momento, el filósofo estaba sentado en la cima de una montaña alta, mirando los cambios en las nebulosas del universo, mirando los insectos que se arrastraban por el suelo, de repente se volvió, asintió y sonrió a nuestro grupo de pastoreo. ganado vacuno y ovino. Cogió un trozo de hierba, lo sostuvo suavemente en su boca, cerró los ojos y lo probó con atención. Me pregunto cuál sería el sabor de este trozo de hierba en la boca del filósofo. Sin embargo, siempre tenía una sonrisa de satisfacción en su rostro.
Conocer y comprender el mundo atómico microscópico de DELPHI puede permitirnos comprender a fondo la estructura de aplicación macroscópica de DELPHI, desarrollando así nuestro software en un espacio ideológico más amplio. Es como si Newton descubriera el movimiento de los objetos macroscópicos, pero estuviera preocupado porque no podía entender por qué los objetos se movían así. Por el contrario, Einstein experimentó la vida feliz de la relatividad entre las leyes de las partículas básicas y el movimiento de los objetos macroscópicos. !
Sección 1 Átomo objeto
¿Qué es TObject?
Es el núcleo básico de la arquitectura del lenguaje Object Pascal y el origen de varios controles VCL. Podemos pensar en TObject como uno de los átomos que componen una aplicación DELPHI. Por supuesto, están formados por partículas más sutiles, como elementos básicos de sintaxis de Pascal.
Se dice que TObject es el átomo del programa DELPHI porque TObject es compatible internamente con el compilador DELPHI. Todas las clases de objetos se derivan de TObject, incluso si no especifica TObject como clase antecesora. TObject se define en la unidad del sistema, que forma parte del sistema. Al comienzo de la unidad System.pas, se encuentra este texto de comentario:
{PConstantes, tipos, procedimientos redefinidos, }
{ y funciones (como True, Integer o }
{Writeln) no tienen declaraciones reales.}
{En lugar de eso, están integrados en el compilador}
{ y son tratados como si fueran declarados }
{ al principio de la unidad del sistema }
Significa que esta unidad contiene constantes, tipos, procedimientos y funciones predefinidos (como: True, Integer o Writeln). En realidad, no están declarados, pero el compilador los incorpora y se considera que se utilizan al comienzo de la compilación. ser una definición establecida. Puede agregar otros archivos de programa fuente como Classes.pas o Windows.pas a su archivo de proyecto para compilar y depurar el código fuente, ¡pero no puede agregar el archivo de programa fuente System.pas a su archivo de proyecto para su compilación! ¡DELPHI informará errores de compilación para definiciones duplicadas de System!
Por lo tanto, TObject es una definición proporcionada internamente por el compilador. Para aquellos de nosotros que usamos DELPHI para desarrollar programas, TObject es algo atómico.
La definición de TObject en la unidad del Sistema es la siguiente:
TObjeto = clase
constructor Crear;
procedimiento Gratis;
función de clase InitInstance (Instancia: Puntero): TObject;
procedimiento CleanupInstance;
función Tipo de clase: TClass;
función de clase ClassName: ShortString;
función de clase ClassNameIs (nombre constante: cadena): booleano;
función de clase ClassParent: TClass;
función de clase ClassInfo: puntero;
función de clase Tamaño de instancia: Entero largo;
función de clase Hereda de (AClass: TClass): booleano;
función de clase Dirección de método (nombre constante: cadena corta): puntero;
función de clase Nombre del método (Dirección: Puntero): Cadena corta;
función Dirección de campo (nombre constante: cadena corta): puntero;
función GetInterface (const IID: TGUID; out Obj): booleano;
función de clase GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
función de clase GetInterfaceTable: PInterfaceTable;
función SafeCallException(ExceptObject: TObject;
ExceptAddr: Puntero): HResult virtual;
procedimiento Después de la Construcción;
procedimiento Antes de la Destrucción; virtual;
procedimiento Despacho(var Mensaje);
procedimiento DefaultHandler(var Mensaje);
función de clase NewInstance: TObject;
procedimiento FreeInstance;
destructor Destruir; virtual;
fin;
A continuación, llamaremos gradualmente a la puerta de los átomos del objeto para ver qué estructura hay en su interior.
Sabemos que TObject es la clase básica de todos los objetos, entonces, ¿qué es exactamente un objeto?
¡Cualquier objeto en DELPHI es un puntero, que indica el espacio ocupado por el objeto en la memoria! Aunque el objeto es un puntero, cuando nos referimos a los miembros del objeto, no necesitamos escribir el código MyObject^.GetName, solo podemos escribir MyObject.GetName. Esta es una sintaxis expandida del lenguaje Object Pascal. soportado por el compilador. Los amigos que usan C++ Builder tienen muy clara la relación entre objetos y punteros, porque los objetos en C++ Builder deben definirse como punteros. El lugar señalado por el puntero del objeto es el espacio del objeto donde el objeto almacena datos. Analicemos la estructura de datos del espacio de memoria señalado por el puntero del objeto.
Los primeros 4 bytes del espacio de objetos apuntan a la tabla de direcciones de métodos virtuales (VMT – Virtual Method Table) de la clase de objeto. El siguiente espacio es el espacio para almacenar los datos de los miembros del objeto en sí, y se almacena en el orden total desde los miembros de datos de la clase ancestral más primitiva del objeto hasta los miembros de datos de la clase de objeto, y en el orden en que Los miembros de datos se definen en cada nivel de clase.
La tabla de métodos virtuales (VMT) de una clase contiene las direcciones de procedimiento de los métodos virtuales de todas las clases derivadas de la clase antecesora original de la clase. El método virtual de una clase es un método declarado con la palabra reservada virtual. El método virtual es el mecanismo básico para lograr el polimorfismo de objetos. Aunque los métodos dinámicos declarados con la palabra reservada dinámica también pueden lograr polimorfismo de objetos, dichos métodos no se almacenan en la tabla de direcciones de métodos virtuales (VMT). Es solo otro método proporcionado por Object Pascal que puede ahorrar espacio de almacenamiento de clases. pero a expensas de la velocidad de llamada.
Incluso si no definimos ningún método virtual de la clase nosotros mismos, el objeto de la clase todavía tiene un puntero a la tabla de direcciones del método virtual, pero la longitud de la entrada de dirección es cero. Sin embargo, ¿dónde se almacenan los métodos virtuales definidos en TObject, como Destroy, FreeInstance, etc.? Resulta que las direcciones de sus métodos se almacenan en un desplazamiento espacial en la dirección negativa con respecto al puntero VMT. De hecho, el espacio de datos desplazado en 76 bytes en la dirección negativa de la tabla VMT es la estructura de datos del sistema de la clase de objeto. Estas estructuras de datos están relacionadas con el compilador y pueden cambiarse en futuras versiones de DELPHI.
Por lo tanto, se puede pensar que VMT es una estructura de datos que comienza desde el espacio de direcciones de desplazamiento negativo. El área de datos de desplazamiento negativo es el área de datos del sistema de VMT, y los datos de desplazamiento positivo de VMT son el área de datos del usuario (método virtual personalizado). tabla de direcciones). Las funciones y procedimientos relacionados con la información de clase o la información de tiempo de ejecución del objeto definidos en TObject generalmente están relacionados con los datos del sistema de VMT.
Un dato de VMT representa una clase. De hecho, ¡VMT es una clase! En Object Pascal, utilizamos identificadores como TObject, TComponent, etc. para representar clases, que se implementan como sus respectivos datos VMT internamente en DELPHI. El tipo de clase definida con la clase de palabra reservada es en realidad un puntero a los datos VMT relevantes.
Para nuestra aplicación, los datos VMT son datos estáticos. Después de que el compilador compila nuestra aplicación, esta información de datos se determina e inicializa. Las declaraciones de programa que escribimos pueden acceder a información relacionada con VMT, obtener información como el tamaño del objeto, el nombre de la clase o los datos de atributos de tiempo de ejecución, o llamar a métodos virtuales o leer el nombre y la dirección del método, etc.
Cuando se genera un objeto, el sistema asignará un espacio de memoria para el objeto y asociará el objeto con la clase relevante. Por lo tanto, los primeros 4 bytes en el espacio de datos asignado para el objeto se convierten en punteros a datos de clase VMT.
Echemos un vistazo a cómo nacen y mueren los objetos. Al ver a mi hijo de tres años saltando sobre el césped, es precisamente porque he sido testigo del proceso de nacimiento de la vida que puedo comprender verdaderamente el significado y la grandeza de la vida. Sólo aquellos que han experimentado la muerte comprenderán y apreciarán más la vida. Entonces, ¡comprendamos el proceso de creación y muerte de los objetos!
Todos sabemos que el objeto más simple se puede construir utilizando la siguiente declaración:
UnObjeto := TObject.Create;
El compilador implementa su compilación como:
Según el VMT correspondiente a TObject, llame al constructor Crear de TObject. El constructor Create llama al proceso ClassCreate del sistema, y el proceso ClassCreate del sistema llama al método virtual NewInstance a través de la clase VMT almacenada en él. El propósito de llamar al método NewInstance es establecer el espacio de instancia del objeto. Debido a que no hemos sobrecargado este método, es el NewInstance de la clase TObject. El método NewInstance de la clase TObjec llamará al procedimiento GetMem para asignar memoria para el objeto según el tamaño de la instancia del objeto (InstanceSize) inicializado por el compilador en la tabla VMT y luego llamará al método InitInstance para inicializar el espacio asignado. El método InitInstance primero inicializa los primeros 4 bytes del espacio de objetos en un puntero al VMT correspondiente a la clase de objeto y luego borra el espacio restante. Después de establecer la instancia del objeto, también se llama a un método virtual AfterConstruction. Finalmente, guarde el puntero de dirección de los datos de la instancia del objeto en la variable AnObject y de esta manera nace el objeto AnObject.
De manera similar, un objeto se puede destruir usando la siguiente declaración:
UnObjeto.Destruir;
El destructor de TObject, Destroy, se declara como un método virtual, que también es uno de los métodos virtuales inherentes al sistema. El método Destory primero llama al método virtual BeforeDestruction y luego llama al proceso ClassDestroy del sistema. El proceso ClassDestory llama al método virtual FreeInstance a través de la clase VMT, y el método FreeInstance llama al proceso FreeMem para liberar el espacio de memoria del objeto. Así, un objeto desaparece del sistema.
El proceso de destrucción de objetos es más simple que el proceso de construcción de objetos, al igual que el nacimiento de la vida es un proceso de gestación largo, pero la muerte es relativamente corta. Esto parece ser una regla inevitable.
Durante el proceso de construcción y destrucción del objeto, se llaman dos funciones virtuales, NewInstance y FreeInstance, para crear y liberar el espacio de memoria de la instancia del objeto. La razón por la que estas dos funciones se declaran como funciones virtuales es para permitir que los usuarios tengan espacio para la expansión al escribir clases de objetos especiales que requieren que los usuarios administren su propia memoria (como en algunos programas especiales de control industrial).
Declarar AfterConstruction y BeforeDestruction como funciones virtuales también le da a la clase derivada en el futuro la oportunidad de permitir que el objeto recién nacido respire el primer soplo de aire fresco después de generar el objeto y permitir que el objeto complete las consecuencias antes de que muera. Todo esto es algo que tiene sentido. De hecho, el evento OnCreate y el evento OnDestroy del objeto TForm y el objeto TDataModule se activan respectivamente en los dos procesos de funciones virtuales de sobrecarga de TForm y TDataModule.
Además, TObjec también proporciona un método gratuito, que no es un método virtual. Se proporciona especialmente para liberar objetos de forma segura cuando no está claro si está vacío (nulo). De hecho, si no puede determinar si el objeto está vacío, existe un problema de lógica del programa poco clara. Sin embargo, nadie es perfecto y puede cometer errores. También es bueno utilizar Gratis para evitar errores accidentales. Sin embargo, escribir programas correctos no puede depender únicamente de tales soluciones. ¡El primer objetivo de la programación debe ser garantizar la corrección lógica del programa!
Los amigos interesados pueden leer el código original de la unidad del sistema, donde una gran cantidad de código está escrito en lenguaje ensamblador. Los amigos cuidadosos pueden encontrar que el constructor Create y el destructor Destory de TObject no han escrito ningún código. De hecho, a través de la ventana Debug CPU en el estado de depuración, el código ensamblador de Create y Destory se puede reflejar claramente. Porque los maestros que crearon DELPHI no querían proporcionar a los usuarios demasiadas cosas complicadas. Querían que los usuarios escribieran aplicaciones basadas en conceptos simples y ocultaran el trabajo complejo dentro del sistema para que ellos lo realizaran. Por lo tanto, al publicar la unidad System.pas, los códigos de estas dos funciones se eliminan especialmente para que los usuarios piensen que TObject es la fuente de todas las cosas, y las clases derivadas del usuario comienzan completamente desde la nada. Aunque leer estos códigos más esenciales de DELPHI requiere una pequeña cantidad de conocimiento del lenguaje ensamblador, leer dichos códigos puede brindarnos una comprensión más profunda del origen y desarrollo del mundo DELPHI. Incluso si no comprende mucho, ser capaz de comprender al menos algunas cosas básicas nos será de gran ayuda a la hora de escribir programas DELPHI.
Sección 2 Átomo de clase T
En la unidad System.pas, TClass se define así:
TClass = clase de TObject;
Significa que TClass es la clase de TObject. Debido a que TObject en sí es una clase, TClass es la llamada clase de clases.
Conceptualmente, TClass es un tipo de clase, es decir, una clase. Sin embargo, sabemos que una clase de DELPHI representa un dato de VMT. Por lo tanto, la clase puede considerarse como el tipo definido para el elemento de datos VMT. De hecho, es un tipo de puntero que apunta a los datos VMT.
En el lenguaje C++ tradicional anterior, no se podía definir el tipo de clase. Una vez que se compila el objeto, se repara, la información estructural de la clase se convierte en código de máquina absoluto y la información completa de la clase no existirá en la memoria. Algunos lenguajes orientados a objetos de nivel superior pueden admitir el acceso dinámico y la invocación de información de clase, pero a menudo requieren un mecanismo de interpretación interno complejo y más recursos del sistema. El lenguaje Object Pascal de DELPHI absorbe algunas de las excelentes características de los lenguajes orientados a objetos de alto nivel, al tiempo que conserva la ventaja tradicional de compilar programas directamente en código de máquina, lo que resuelve perfectamente los problemas de funciones avanzadas y eficiencia del programa.
Es precisamente porque DELPHI retiene información completa de la clase en la aplicación que puede proporcionar funciones avanzadas orientadas a objetos, como convertir e identificar clases en tiempo de ejecución, en las que los datos VMT de la clase desempeñan un papel central clave. Los amigos interesados pueden leer los dos procesos de ensamblaje de AsClass e IsClass en la unidad del sistema. Son los códigos de implementación de los operadores as e is para profundizar su comprensión de las clases y los datos VMT.
El mundo atómico de DELPHI (2)
Palabras clave: controles Delphi varios
Sección 2 Átomo de clase T
En la unidad System.pas, TClass se define así:
TClass = clase de TObject;
Significa que TClass es la clase de TObject. Debido a que TObject en sí es una clase, TClass es la llamada clase de clases.
Conceptualmente, TClass es un tipo de clase, es decir, una clase. Sin embargo, sabemos que una clase de DELPHI representa un dato de VMT. Por lo tanto, la clase puede considerarse como el tipo definido para el elemento de datos VMT. De hecho, es un tipo de puntero que apunta a los datos VMT.
En el lenguaje C++ tradicional anterior, no se podía definir el tipo de clase. Una vez que se compila el objeto, se repara, la información estructural de la clase se convierte en código de máquina absoluto y la información completa de la clase no existirá en la memoria. Algunos lenguajes orientados a objetos de nivel superior pueden admitir el acceso dinámico y la invocación de información de clase, pero a menudo requieren un mecanismo de interpretación interno complejo y más recursos del sistema. El lenguaje Object Pascal de DELPHI absorbe algunas de las excelentes características de los lenguajes orientados a objetos de alto nivel, al tiempo que conserva la ventaja tradicional de compilar programas directamente en código de máquina, lo que resuelve perfectamente los problemas de funciones avanzadas y eficiencia del programa.
Es precisamente porque DELPHI retiene información completa de la clase en la aplicación que puede proporcionar funciones avanzadas orientadas a objetos, como convertir e identificar clases en tiempo de ejecución, en las que los datos VMT de la clase desempeñan un papel central clave. Los amigos interesados pueden leer los dos procesos de ensamblaje de AsClass e IsClass en la unidad del sistema. Son los códigos de implementación de los operadores as e is para profundizar su comprensión de las clases y los datos VMT.
Con el tipo de clase, puedes usar la clase como variable. Una variable de clase puede entenderse como un objeto especial y puede acceder a los métodos de una variable de clase como un objeto. Por ejemplo: echemos un vistazo al siguiente fragmento de programa:
tipo
TSampleClass = clase de TSampleObject;
TSampleObject = clase (TObject)
público
constructor Crear;
destructor Destruir; anular;
función de clase GetSampleObjectCount:Integer;
procedimiento GetObjectIndex:Integer;
fin;
var
aSampleClass: TSampleClass;
unaClase: TClass;
En este código, definimos una clase TSampleObject y su tipo de clase relacionada TSampleClass, así como dos variables de clase aSampleClass y aClass. Además, también definimos un constructor, un destructor, un método de clase GetSampleObjectCount y un método de objeto GetObjectIndex para la clase TSampleObject.
Primero, comprendamos el significado de las variables de clase aSampleClass y aClass.
Obviamente, puede tratar TSampleObject y TObject como valores constantes y asignarlos a variables aClass, al igual que asignar 123 valores constantes a la variable entera i. Por lo tanto, la relación entre tipos de clase, clases y variables de clase es la relación entre tipos, constantes y variables, pero a nivel de clase en lugar de a nivel de objeto. Por supuesto, no es legal asignar TObject directamente a aSampleClass, porque aSampleClass es una variable de clase de la clase TSampleObject derivada de TObject, y TObject no contiene todas las definiciones compatibles con el tipo TSampleClass. Por el contrario, es legal asignar TSampleObject a una variable Class, porque TSampleObject es una clase derivada de TObject y es compatible con el tipo TClass. Esto es exactamente similar a la asignación y la relación de coincidencia de tipos de variables de objeto.
Luego, echemos un vistazo a qué son los métodos de clase.
El llamado método de clase se refiere al método llamado a nivel de clase, como el método GetSampleObjectCount definido anteriormente, que es un método declarado con la palabra reservada clase. Los métodos de clase son diferentes de los métodos de objeto llamados a nivel de objeto. Los métodos de objeto ya nos son familiares, y los métodos de clase siempre se utilizan en el nivel de acceso y control de las características comunes de todos los objetos de clase y en la gestión central de objetos. En la definición de TObject podemos encontrar una gran cantidad de métodos de clase, como ClassName, ClassInfo, NewInstance, etc. Entre ellos, NewInstance también se define como virtual, es decir, un método de clase virtual. Esto significa que puede reescribir el método de implementación de NewInstance en una subclase derivada para construir instancias de objetos de esa clase de una manera especial.
También puede utilizar el identificador self en los métodos de clase, pero su significado es diferente del de self en los métodos de objeto. El self en el método de clase representa su propia clase, es decir, el puntero al VMT, mientras que el self en el método de objeto representa el objeto en sí, es decir, el puntero al espacio de datos del objeto. Aunque los métodos de clase solo se pueden usar a nivel de clase, aún puedes llamar a métodos de clase a través de un objeto. Por ejemplo, el método de clase ClassName del objeto TObject se puede llamar mediante la declaración aObject.ClassName, porque los primeros 4 bytes en el espacio de datos del objeto señalado por el puntero del objeto son punteros a la clase VMT. Por el contrario, no se pueden llamar métodos de objetos a nivel de clase y declaraciones como TObject.Free deben ser ilegales.
¡Vale la pena señalar que el constructor es un método de clase y el destructor es un método de objeto!
¿Qué? ¡Los constructores son métodos de clase y los destructores son métodos de objetos! ¿Hubo algún error?
Verá, cuando crea un objeto, claramente usa una declaración similar a la siguiente:
unObjeto := TObject.Create;
Claramente está llamando al método Create de la clase TObject. Al eliminar un objeto, utilice la siguiente declaración:
unObjeto.Destruir;
Incluso si utiliza el método Free para liberar el objeto, se llama indirectamente al método Destroy del objeto.
La razón es muy simple: antes de que se construya el objeto, el objeto aún no existe, solo existe la clase. Solo puedes usar métodos de clase para crear objetos. Por el contrario, eliminar un objeto debe eliminar el objeto existente. El objeto se libera, no la clase.
Finalmente, analicemos el tema de los constructores ficticios.
En el lenguaje C++ tradicional, se pueden implementar destructores virtuales, pero implementar constructores virtuales es un problema difícil. Porque, en el lenguaje C++ tradicional, no hay tipos de clases. Las instancias de objetos globales existen en el espacio de datos global en el momento de la compilación, y los objetos locales de funciones también son instancias asignadas en el espacio de la pila en el momento de la compilación. Incluso los objetos creados dinámicamente se colocan en la estructura de clase fija utilizando el nuevo operador. en el espacio del montón, y el constructor es solo un método de objeto que inicializa la instancia del objeto generado. No existen métodos de clase reales en el lenguaje C++ tradicional. Incluso si se pueden definir los llamados métodos estáticos basados en clases, en última instancia se implementan como una función global especial, sin mencionar que los métodos de clases virtuales solo pueden apuntar a objetos específicos. instancias eficientes. Por lo tanto, el lenguaje C ++ tradicional cree que antes de generar una instancia de objeto específica, es imposible construir el objeto en sí en función del objeto que se generará. ¡De hecho, es imposible, porque esto crearía una paradoja lógica contradictoria!
Sin embargo, es precisamente debido a los conceptos clave de información de tipo de clase dinámica, métodos de clase verdaderamente virtuales y constructores implementados en base a clases en DELPHI que se pueden implementar constructores virtuales. Los objetos son producidos por clases. El objeto es como un bebé en crecimiento, y la clase es su madre. El bebé mismo no sabe en qué tipo de persona se convertirá en el futuro, pero las madres utilizan sus propios métodos educativos para cultivar a diferentes niños. . Gente, los principios son los mismos.
Es en la definición de la clase TComponent que el constructor Create se define como virtual para que diferentes tipos de controles puedan implementar sus propios métodos de construcción. Ésta es la grandeza de conceptos como las clases creadas por TClass, y también la grandeza de DELPHI.
................................................. ..
Capítulo 3 La visión del tiempo y el espacio en WIN32
Mi anciano padre miró a su nieto pequeño jugando con juguetes en el suelo y luego me dijo: "Este niño es como tú cuando eras niño. Le gusta desarmar las cosas y sólo se detiene después de verlas hasta el final. " Pensando en cuando era niño, a menudo desmantelaba carritos de juguete, pequeños despertadores, cajas de música, etc., y mi madre a menudo me regañaba.
La primera vez que entendí los principios básicos de las computadoras tuvo que ver con una caja de música que desarmé. Fue en un cómic cuando estaba en la escuela secundaria. Un anciano con barba blanca explicaba la teoría de las máquinas inteligentes y un tío con bigote hablaba de computadoras y cajas de música. Dijeron que la unidad central de procesamiento de una computadora es la fila de lengüetas musicales que se usan para la pronunciación en la caja de música, y el programa de computadora son las protuberancias densamente empaquetadas en el pequeño cilindro de la caja de música. La rotación del pequeño cilindro es equivalente. a la rotación de la unidad central de procesamiento. El movimiento natural del puntero de instrucción, mientras que los golpes que representan la música en el pequeño cilindro controlan la vibración de la lengüeta musical para producir instrucciones equivalentes a la ejecución del programa por parte del procesador central. La caja de música emite una hermosa melodía, que se reproduce de acuerdo con la partitura grabada en el pequeño cilindro por el artesano. La computadora completa un procesamiento complejo basado en el programa preprogramado por el programador. Después de ir a la universidad, aprendí que el anciano de barba blanca era el gigante científico Turing. Su teoría de los autómatas finitos promovió el desarrollo de toda la revolución de la información, y el tío del bigote era el padre de las computadoras, von Neumann. La arquitectura informática sigue siendo la principal estructura arquitectónica de las computadoras. La caja de música no fue desmantelada en vano, mamá puede estar tranquila.
Sólo con una comprensión simple y profunda podemos crear creaciones profundas y concisas.
En este capítulo discutiremos los conceptos básicos relacionados con nuestra programación en el sistema operativo Windows de 32 bits y estableceremos la visión correcta del tiempo y el espacio en WIN32. Espero que después de leer este capítulo, podamos tener una comprensión más profunda de los programas, procesos y subprocesos, comprender los principios de los archivos ejecutables, las bibliotecas de enlaces dinámicos y los paquetes de tiempo de ejecución, y ver claramente la verdad sobre los datos globales, los datos locales y los parámetros en la memoria. .
Sección 1 Comprender el proceso
Por razones históricas, Windows se originó a partir de DOS. En la era DOS, siempre tuvimos solo el concepto de programa, pero no el concepto de proceso. En aquella época, sólo los sistemas operativos normales, como UNIX y VMS, tenían el concepto de procesos, y multiproceso significaba miniordenadores, terminales y múltiples usuarios, lo que también significaba dinero. La mayor parte del tiempo solo podía usar microcomputadoras y sistemas DOS relativamente baratos. Solo comencé a entrar en contacto con procesos y minicomputadoras cuando estaba estudiando sistemas operativos.
Fue sólo después de Windows 3. En el pasado, en DOS solo se podía ejecutar un programa al mismo tiempo, pero en Windows se podían ejecutar varios programas al mismo tiempo. Mientras se ejecuta un programa en DOS, el mismo programa no se puede ejecutar al mismo tiempo, pero en Windows, se pueden ejecutar más de dos copias del mismo programa al mismo tiempo, y cada copia en ejecución del programa es un proceso. Para ser más precisos, cada ejecución de cualquier programa genera una tarea y cada tarea es un proceso.
Cuando los programas y procesos se entienden juntos, se puede considerar que la palabra programa se refiere a cosas estáticas. Un programa típico es código estático y datos compuestos por un archivo EXE o un archivo EXE más varios archivos DLL. Un proceso es la ejecución de un programa, que es código y datos que cambian dinámicamente y que se ejecutan dinámicamente en la memoria. Cuando se requiere la ejecución de un programa estático, el sistema operativo proporcionará un cierto espacio de memoria para esta operación, transferirá el código y los datos del programa estático a estos espacios de memoria y reposicionará y asignará el código y los datos del programa en este espacio. ejecutado en su interior, creando así un proceso dinámico.
Dos copias del mismo programa ejecutándose al mismo tiempo significan que hay dos espacios de proceso en la memoria del sistema, pero las funciones de su programa son las mismas, pero se encuentran en diferentes estados que cambian dinámicamente.
En términos del tiempo de ejecución del proceso, cada proceso se ejecuta al mismo tiempo. El término profesional se denomina ejecución paralela o ejecución concurrente. Pero esta es principalmente la sensación superficial que nos da el sistema operativo. De hecho, cada proceso se ejecuta en tiempo compartido, es decir, cada proceso se turna para ocupar el tiempo de la CPU para ejecutar las instrucciones del programa del proceso. Para una CPU, solo se ejecutan al mismo tiempo las instrucciones de un proceso. El sistema operativo es el manipulador detrás de la operación del proceso programado. Guarda y cambia constantemente el estado actual de cada proceso ejecutado en la CPU, de modo que cada proceso programado piense que se está ejecutando de forma completa y continua. Dado que la programación de procesos en tiempo compartido es muy rápida, nos da la impresión de que todos los procesos se están ejecutando al mismo tiempo. De hecho, una verdadera operación simultánea sólo es posible en un entorno de hardware con múltiples CPU. Cuando hablemos de subprocesos más adelante, encontraremos que los subprocesos son los que realmente impulsan el proceso y, lo que es más importante, proporcionan espacio para el proceso.
En términos del espacio ocupado por el proceso, cada espacio de proceso es relativamente independiente y cada proceso se ejecuta en su propio espacio independiente. Un programa incluye tanto espacio de código como espacio de datos. Tanto el código como los datos ocupan espacio de proceso. Windows asigna memoria real para el espacio de datos requerido por cada proceso y generalmente usa métodos compartidos para el espacio de código, asignando un código de un programa a múltiples procesos del programa. Esto significa que si un programa tiene 100 KB de código y requiere 100 KB de espacio de datos, lo que significa que se requiere un total de 200 KB de espacio de proceso, el sistema operativo asignará 200 KB de espacio de proceso la primera vez que se ejecute el programa y 200 KB de espacio de proceso. El espacio se asignará la segunda vez que se ejecute el programa. Cuando se inicia un proceso, el sistema operativo solo asigna 100 K de espacio de datos, mientras que el espacio de código comparte el espacio del proceso anterior.
Lo anterior es la vista básica de tiempo y espacio del proceso en el sistema operativo Windows. De hecho, existe una gran diferencia en la vista de tiempo y espacio del proceso entre los sistemas operativos de 16 y 32 bits de Windows.
En términos de tiempo, la gestión de procesos de los sistemas operativos Windows de 16 bits, como Windows 3.x, es muy sencilla. En realidad, es solo un sistema operativo de gestión multitarea. Además, la programación de tareas del sistema operativo es pasiva. Si una tarea no deja de procesar el mensaje, el sistema operativo debe esperar. Debido a fallas en la gestión de procesos del sistema Windows de 16 bits, cuando un proceso se está ejecutando, ocupa por completo los recursos de la CPU. En aquellos días, para que Windows de 16 bits tuviera la oportunidad de programar otras tareas, Microsoft elogiaba a los desarrolladores de aplicaciones de Windows por ser programadores de mentalidad amplia, por lo que estaban dispuestos a escribir unas cuantas líneas más de código para regalarle al Windows de 16 bits la posibilidad de programar otras tareas. Sistema operativo. Por el contrario, los sistemas operativos WIN32, como Windows 95 y NT, tienen capacidades reales de sistema operativo multiproceso y multitarea. El proceso en WIN32 está completamente programado por el sistema operativo. Una vez que finaliza el intervalo de tiempo del proceso en ejecución, el sistema operativo cambiará activamente al siguiente proceso independientemente de si el proceso todavía está procesando datos. Estrictamente hablando, el sistema operativo Windows de 16 bits no puede considerarse como un sistema operativo completo, pero el sistema operativo Win32 de 32 bits es el verdadero sistema operativo. Por supuesto, Microsoft no dirá que Win32 compensa las deficiencias de las ventanas de 16 bits, pero afirma que Win32 implementa una tecnología avanzada llamada "multitarea preventiva", que es un método comercial.
Desde una perspectiva espacial, aunque el espacio de proceso en el sistema operativo de Windows de 16 bits es relativamente independiente, los procesos pueden acceder fácilmente al espacio de datos de los demás. Debido a que estos procesos son en realidad diferentes segmentos de datos en el mismo espacio físico, y las operaciones de dirección inadecuadas pueden causar fácilmente lectura y escritura de espacio incorrectos, y bloquear el sistema operativo. Sin embargo, en el sistema operativo Win32, cada espacio de proceso es completamente independiente. Win32 proporciona a cada proceso un espacio de direcciones virtual y continuo de hasta 4G. El llamado espacio de direcciones continua significa que cada proceso tiene un espacio de direcciones de $ 00000000 a $ FFFFFFFF, en lugar del espacio segmentado de ventanas de 16 bits. En Win32, no tiene que preocuparse por sus operaciones de lectura y escritura que afectan involuntariamente los datos en otros espacios de proceso, y no tiene que preocuparse por otros procesos que vienen a acosar su trabajo. Al mismo tiempo, el espacio virtual 4G continuo proporcionado por Win32 para su proceso es la memoria física asignada por el sistema operativo con el soporte del hardware. . Memoria física.
Sección 2 Espacio de proceso
Cuando usamos Delphi para escribir aplicaciones WIN32, rara vez nos preocupamos por el mundo interno del proceso cuando se está ejecutando. Debido a que Win32 proporciona 4G de espacio de proceso virtual continuo para nuestro proceso, quizás la aplicación más grande del mundo actualmente solo usa parte de él. Parece que el espacio del proceso es ilimitado, pero el espacio de proceso 4G es virtual, y la memoria real de su máquina puede estar lejos de esto. Aunque el proceso tiene un espacio tan vasto, algunos programas de algoritmos complejos aún no podrán ejecutarse debido al desbordamiento de la pila, especialmente los programas que contienen una gran cantidad de algoritmos recursivos.
Por lo tanto, una comprensión profunda de la estructura del espacio de proceso 4G, su relación con la memoria física, etc. nos ayudará a comprender el mundo del espacio-tiempo de Win32 más claramente, para que podamos usar los métodos correctos en el trabajo de desarrollo real . Vista mundial y metodología para resolver varios problemas difíciles.
A continuación, utilizaremos un experimento simple para comprender el mundo interno del espacio de proceso de Win32. Esto puede requerir algún conocimiento de los registros de copa y el lenguaje de ensamblaje, pero traté de explicarlo en un lenguaje simple.
Cuando se inicia Delphi, se generará automáticamente un proyecto Project1 y comenzaremos con él. Establezca un punto de interrupción en cualquier parte del programa original de Project1.PRIS, por ejemplo, establezca un punto de interrupción al principio. Luego ejecute el programa y se detendrá automáticamente cuando alcance el punto de interrupción. En este momento, podemos abrir la ventana de la CPU en la herramienta de depuración para observar la estructura interna del espacio de proceso.
El EIP de registro de puntero de instrucción actual se detiene a $ 0043E4B8. Espacio de proceso, que ocupa $ 00000000 a un pequeño espacio de direcciones para $ FFFFFFFF.
En el cuadro de comando en la ventana CPU, puede observar el contenido del espacio de proceso. Al ver el contenido del espacio de menos de $ 00400000, encontrará una serie de signos de interrogación "? Si observa el valor hexadecimal de la variable global hinstance en este momento, encontrará que también es $ 00400000. Aunque Hinstance refleja el mango de la instancia del proceso, de hecho, es el valor de la dirección inicial cuando el programa se carga en la memoria, también en ventanas de 16 bits. Por lo tanto, podemos pensar que el programa del proceso se carga a partir de $ 00400000, es decir, el espacio que comienza desde 4m en el espacio virtual 4G es el espacio donde se carga el programa.
Desde $ 00400000 en adelante y antes de $ 0044d000, es principalmente el espacio de direcciones del código del programa y los datos globales. En el cuadro de pila en la ventana CPU, puede ver la dirección de la pila actual. Del mismo modo, encontrará que el espacio actual de direcciones de pila es de $ 0067b000 a $ 00680000, con una longitud de $ 5000. De hecho, el tamaño mínimo de espacio de pila del proceso es de $ 5000, que se obtiene en función del valor de tamaño de pila MIN establecido en la página de enlazador de proyectos al compilar el programa Delphi, más $ 1000. La pila crece desde la dirección de alta gama hasta la parte inferior. espacio de proceso. Al compilar un programa Delphi, puede controlar el espacio máximo de pila que se puede aumentar estableciendo el valor del tamaño de pila máxima en la página de enlazador en ProyectOptions. Especialmente en programas que contienen relaciones de llamadas de subrutina profundas o usan algoritmos recursivos, el valor del tamaño de la pila máxima debe establecerse razonablemente. Debido a que llamar a una subrutina requiere espacio de pila, y después de agotarse la pila, el sistema lanzará un error de "desbordamiento de pila".
Parece que el espacio de proceso después del espacio de la pila debería ser espacio libre. De hecho, este no es el caso. Parece que el proceso realmente puede tener solo un espacio 2G. De hecho, el espacio que realmente puede poseer un proceso ni siquiera es 2G, porque el espacio de 4m de $ 00000000 a $ 00400000 también es un área restringida.
Pero pase lo que pase, las direcciones que nuestro proceso puede usar todavía son muy amplias. Especialmente después del espacio de la pila y entre $ 80,000,000, es el campo de batalla principal del espacio de proceso. El espacio de memoria asignado por el proceso desde el sistema se asignará a este espacio, la biblioteca de enlaces dinámicos cargados por el proceso se asignará a este espacio, el espacio de pila de subprocesos del nuevo hilo también se asignará a este espacio, casi todo Las operaciones que involucran la asignación de memoria se asignarán a este espacio. Tenga en cuenta que la asignación mencionada aquí significa la correspondencia entre la memoria real y este espacio virtual. ???? ".