Manejar la memoria en VB
Este texto está dedicado a la chica más maravillosa del mundo, mi inspiración y mi fuerza, aunque sé que probablemente nunca lo lea ni sepa que existe.
ICH LIEBE DICH...
Índice1. Nociones digitales
2. Representación de datos binarios
3. Arquitectura de memoria
4. Argumentos como referencia o como valor
5. Comportamiento de Strings en memoria
6. Funciones de manejo de memoria
6.1 CopyMemory
6.2 CopyMemory y Strings
6.3 Asignar y liberar memoria
6.3.1 Distribución de la memoria virtual
6.3.2 Espacio de Direcciones Virtuales y Almacenamiento Físico
6.3.3 Estado de las páginas
6.3.4 Alcance de la memoria asignada
6.3.5 Funciones VirtualAlloc y VirtualFree
6.4 Protección de memoria
6.4.1 Modos de protección: VirtualProtect
6.4.2 VirtualLock, VirtualUnlock
6.5 Leer y escribir memoria de distintos procesos
6.6 Memoria compartida
6.6.1 Concepto. Importancia de la memoria compartida. Ejemplos.
6.6.2 Asignación de memoria compartida
6.6.3 Escribir, leer en memoria compartida
6.7 Validar datos y accesos en memoria
6.8 Consultar estado y datos de regiones de memoria: VirtualQueryEx
6.9 Otras funciones de memoria: ZeroMemory, FillMemory, GlobalMemoryStatus
7. Usar la memoria en otros procesos: consideraciones y ejemplos.
8. Creando programas para todas las plataformas de Windows.
Algunas veces se lo expliqué a algunos pero mejor lo dejo sentado en un documento así no tengo que volver a repetirlo xD.
Antes de explicar el funcionamiento de las funciones de memoria que nos proporciona el SO, hay que tener presentes algunos conceptos básicos de técnicas digitales y cómo se posicionan los datos en memoria, así que voy a empezar por ahí.
1. Nociones digitalesPrimero, los datos en la memoria física, o sea en la plaqueta que tenemos dentro del gabinete, se guarda en forma binaria, o sea como 1's y 0's, y esto es debido a una razón muy lógica y es que en la electrónica hay dos opciones, hay tensión (un 1) o no (un 0).
Ahora, para los ojos humanos ver millones de 0s y 1s no nos diría nada y poder mostrar eso de alguna manera mediante un programa, requeriría de muchos recursos. Lo que nosotros vemos en los editores de memoria en realidad son bytes, no bits (un 0 o un 1 es un bit).
Un byte es un conjunto de 8 bits, que puede adoptar un valor entre 0 y 255 en el caso de que sea sin signo, o de -128 a 127 para el caso que sea con signo. Muchos se preguntarán el por qué estos valores, y para entender algo es mejor llegar hasta el fondo, así que voy a dar el ejemplo con un nº binario.
Un número de 8 bits visto en binario sería como el siguiente:
Es claro, 8 bits alineados. Entonces, como en el sistema binario sólo se puede trabajar con 0s y 1s, el valor máximo que puede adoptar un número de 8 bits se alcanzará cuando todos los bits estén en 1:
Lo único que nos queda ahora es transformar ese nº binario en decimal, pero primero hay que tener en cuenta unos conceptos:
1. Cada bit tiene un índice de base 0 que lo identifica, siempre contando de izquierda a derecha:
7 6 5 4 3 2 1 0 <- Índices
1 1 1 1 1 1 1 1 <- Bits
Dependiendo de la posición del bit, este valdrá diferente, se dice que cada bit tiene "peso", y el más pesado es el primero de la derecha, esto se debe a que cada posición adopta el valor de 2 (dos) elevado al índice, entonces el valor de cada bit sería:
7 6 5 4 3 2 1 0 <- Índices
128 64 32 16 8 4 2 1 <- Valores
1 1 1 1 1 1 1 1 <- Bits
Esto significa que para el índice 0, el peso del bit será 2^0, para el índice 1, será 2^1, para el 2, 2^2, etc.
Ahora, para terminar la conversión, multiplicamos el valor del bit por el peso que tenga, y eso nos dará el número en decimal:
1*128 + 1*64 + 1*32 + 1*16 + 1*8 + 1*4 + 1*2 + 1*1 = 255
Y voilà, ahí tenemos el 255 que es lo máximo que podemos representar con un nº de 8 bits.
2. Representación de datos binariosUna vez que lo anterior está claro, voy a explicar cómo representan los editores hexadecimales y demás visores de memoria, y el mismo sistema a los datos.
Como dije antes, no hay forma de leer un 1 o un 0 de la memoria si no es aplicando álgebra de boole (AND, OR, etc). El valor mínimo que se puede leer de la memoria es 1 byte, por esta razón la representación que hacen es mostrar los datos con caracteres ascii, ya que estos ocupan 1 byte (valores de 0 a 255). Pero que quede bien claro, TODO LO QUE SEA TEXTO ES PARA MOSTRAR AL USUARIO, incluso esto que estoy escribiendo se guarda en forma binaria, pero se muestra en forma gráfica de manera que sea legible para el ojo humano.
Entonces todos esos "caracteres raros" que vemos al abrir un archivo o al examinar la memoria, no es otra cosa que un conjunto de valores de 1 byte, que por comodidad se muestran gráficamente como un caracter.
3. Arquitectura de memoriaAcá viene la parte más importante que es cómo están dispuestos los datos en la memoria, y el funcionamiento general de el SO en la interacción con esta. Si no leyeron lo anterior les recomiendo que lo lean, si no lo entendieron es mejor que pregunten porque sin tener los conceptos anteriores claros va a ser difícil comprender esta parte.
Otra aclaración, desde esta parte voy a dejar de hablar de bits, y me voy a referir a los bytes ya que así es como trabaja el sistema en general, además se harían muy extensos los ejemplos.
Voy a empezar explicando dos conceptos que adoptan los microprocesadores:
Big Endian y
Little EndianComo todos sabemos, el micro interactúa directamente con la memoria leyendo y escribiendo datos, pero dependiendo de la arquitectura del micro los va a escribir de diferente manera.
Little EndianEn este caso, el microprocesador escribe el byte más bajo de un número en la dirección más baja de memoria. Voy a dar un ejemplo con un número de tipo Long (4 bytes). Para que quede claro mostraré primero cómo estarían repartidos los bits
|Byte 3| |Byte 1| |Byte 2| |Byte 0|
11111111 11111111 11111111 11111111
Una arquitectura Little Endian dispondría un número de estas caractéristicas de la siguiente manera en memoria:
Dirección + 0 Byte 0
Dirección + 1 Byte 1
Dirección + 2 Byte 2
Dirección + 3 Byte 3
Donde Dirección es una dirección cualquiera de memoria. Lo importante a destacar es que el primer byte del número será el primero en memoria. Esta norma la adoptan los microprocesadores de Intel y compatibles (los que la mayoría usamos)
Big EndianAl contrario del caso anterior, esta arquitectura dispone el byte más alto en la dirección de memoria más baja:
Dirección + 0 Byte 3
Dirección + 1 Byte 2
Dirección + 2 Byte 1
Dirección + 3 Byte 0
¿A qué viene todo esto?, muy sencillo, que para saber cómo leer desde la memoria debemos conocer primero cómo guarda los datos, es una cuestión de lógica.
Lo último que queda por decir de este tema es que existen otras unidades de medida además del byte, y que son los
Words y los
DWords o DoubleWords.
Un Word equivale a 2 bytes, y un DWord equivale a 2 Words. ¿Por qué no digo que un DWord son 4 bytes?, porque así van a comprender mejor lo que sigue :P.
Y ya está llegando la parte interesante que nos va a introducir adentro de la programación. Desde acá en adelante voy a explicar todo lo que resta del funcionamiento de memoria pero orientado a Visual Basic, que es lo que nos interesa.
Antes de seguir, voy a poner el código de 6 funciones que nos van a servir para experimentar y ver que lo que estoy explicando en esta guía es verdad.
Property Get HiByte(ByVal Word As Integer) As Byte
' Devuelve el Byte alto del Word especificado.
'
If Word And &H8000 Then
If Not (Word Or &HFF) = &HFFFFFFFF Then Word = (Word Xor &HFF)
HiByte = &H80 Or ((Word And &H7FFF) \ &HFF)
Else
HiByte = Word \ 256
End If
End Property
Property Get HiWord(Dword As Long) As Integer
' Devuelve el Word alto del DWord especificado.
'
If Dword And &H80000000 Then
HiWord = (Dword / 65535) - 1
Else
HiWord = Dword / 65535
End If
End Property
Property Get LoByte(Word As Integer) As Byte
' Devuelve el Byte bajo del Word especificado.
'
LoByte = (Word And &HFF)
End Property
Property Get LoWord(Dword As Long) As Integer
' Devuelve el Word bajo del DWord especificado.
'
If Dword And &H8000& Then
LoWord = &H8000 Or (Dword And &H7FFF&)
Else
LoWord = Dword And &HFFFF&
End If
End Property
Property Get MakeWord(ByVal bHi As Byte, ByVal bLo As Byte) As Integer
' Crea un Word a partir de sus dos componentes Byte.
'
If bHi And &H80 Then
MakeWord = (((bHi And &H7F) * 255) + bLo) Or &H7FFF
Else
MakeWord = (bHi * 255) + bLo
End If
End Property
Property Get MakeDWord(wHi As Integer, wLo As Integer) As Long
' Crea un DWord a partir de sus dos componentes Word.
'
If wHi And &H8000& Then
MakeDWord = (((wHi And &H8000&) * 65536) Or (wLo And &HFFFF&)) Or &H80000000
Else
MakeDWord = (wHi * &H10000) + wLo
End If
End Property
Cada una de las funciones está comentada, lo único que voy a destacar es que un Word (2 bytes) equivale a un Integer que ocupa lo mismo, y un DWord (4 bytes) equivale a un Long. Más adelante vamos a usar estas funciones.
Lo que viene ahora no es exclusivo de VB sino de programación en general, pero será explicado con las matrices de Byte de VB.
Una matiz de Byte no es más que una serie de bytes consecutivos en memoria. Vamos a suponer que tenemos una matriz de 6 bytes, y le asignamos los siguientes valores:
btData(0)=65
btData(1)=66
btData(2)=67
btData(3)=68
btData(4)=69
btData(5)=70
Ahora vamos a suponer que el primero elemento de la matriz está en la dirección de memoria 1000 decimal, entonces en un mapa de memoria estos datos quedarían así:
Pos Valor
1000 65
1001 66
1002 67
1003 68
1004 69
1005 70
El esquema en columna es sólo para demostrar, que cada byte le sucede al anterior pero esto está alineado en realidad:
65 66 67 68 69 70 ' Los espacios sólo son para separar, en la realidad no existen.
Como dije en una parte de este documento, los editores Hexadecimales (no sé por qué le pusieron ese nombre xD) y visores de memoria varios, siempre muestran en una parte el valor numérico y en otra parte el caracter gráfico que le corresponde a ese valor. Entonces si pasamos esta matriz a la forma "visible para el ojo humano", quedaría así:
65 66 67 68 69 70 <- Valor numérico
A B C D E F <- Representación gráfica
Entonces queda claro que una cadena de caracteres no es más que la forma que tiene el sistema operativo de mostrarle gráficamente al usuario una serie de números de 1 byte.
Lo único que falta es entender cómo guarda los DWords y los Words, y cómo se generan. Acá es donde nos van a servir las funciones anteriores.
Tomaremos como ejemplo los primeros 4 elementos de la matriz anterior, 65-66-67-68, para ejemplificar la representación de números de 2 y 4 bytes. Como dije antes, un DWord son 2 Words, y un Word son 2 bytes, por lo tanto un DWord son 2 pares de bytes. Consideremos el esquema siguiente para comprender cómo se forman los números en memoria:
______DWord______
_WordL_ _WordH_
BL BH BL BH
__ __ __ __
65 66 67 68
Donde BL es el 'Byte bajo' del correspondiente Word, BH es el 'Byte alto', WordL es el 'Word bajo' del DWord y WordH es el 'Word alto' del DWord.
Entonces, vamos por partes como dijo jack el destripador xD, el siguiente código primero genera ambos Words y luego el DWord:
Sub TestMem()
Dim btData(3) As Byte
Dim iLoWord As Integer, iHiWord As Integer
Dim lDWord As Long
btData(0) = 65 ' Byte bajo del Word bajo.
btData(1) = 66 ' Byte alto del Word bajo.
btData(2) = 67 ' Byte bajo del Word alto.
btData(3) = 68 ' Byte alto del Word alto.
iLoWord = MakeWord(btData(1), btData(0)) ' Crea el Word bajo.
iHiWord = MakeWord(btData(3), btData(2)) ' Crea el Word alto.
lDWord = MakeDWord(iHiWord, iLoWord) ' Crea el DWord.
Debug.Print Hex$(VarPtr(lDWord))
Stop
End Sub
Ahora vamos a necesitar un editor de memoria, el mejor editor hexa que conozco es el WinHex, pero pueden usar el que más les guste. Este procedimiento crea el DWord por cada una de sus partes, e imprime en la ventana Inmediato la dirección de la variable lDWord en hexadecimal (sólo por conveniencia porque las direcciones de memoria son números muy grandes).
La función no-documentada de VB VarPtr() nos devuelve el puntero a cualquier variable. El puntero a una variable es la dirección en memoria del primer byte de los datos, se llama puntero por la razón obvia, que apunta al primer byte.
Si con el editor hexadecimal nos dirijimos a la dirección que se imprime, y vemos la parte de los valores hexadecimales, veremos 4 bytes seguidos, 65-66-67-68.
Para el que no quiera usar un editor hexadecimal o no lo tenga, escribí otras 2 funciones que convierten de Texto a DWord y de DWord a Texto, tal y como funciona en memoria:
Function DWordToString(dw As Long) As String
DWordToString = Chr$(LoByte(LoWord(dw))) & _
Chr$(HiByte(LoWord(dw))) & _
Chr$(LoByte(HiWord(dw))) & _
Chr$(HiByte(HiWord(dw)))
End Function
Function StringToDWord(Str As String) As Long
If Len(Str) < 4 Then Str = Str & String$(4 - Len(Str), 0)
StringToDWord = MakeDWord( _
MakeWord( _
Asc(Mid$(Str, 4)), _
Asc(Mid$(Str, 3, 1)) _
), _
MakeWord( _
Asc(Mid$(Str, 2, 1)), _
Asc(Mid$(Str, 1, 1)) _
) _
)
End Function
Ahora cambiemos la linea que imprime la dirección de la variable lDWord por la llamada a DWordToString(), para que vean lo que pasa :).
Sub TestMem()
Dim btData(3) As Byte
Dim iLoWord As Integer, iHiWord As Integer
Dim lDWord As Long
btData(0) = 65 ' Byte bajo del Word bajo.
btData(1) = 66 ' Byte alto del Word bajo.
btData(2) = 67 ' Byte bajo del Word alto.
btData(3) = 68 ' Byte alto del Word alto.
iLoWord = MakeWord(btData(1), btData(0)) ' Crea el Word bajo.
iHiWord = MakeWord(btData(3), btData(2)) ' Crea el Word alto.
lDWord = MakeDWord(iHiWord, iLoWord) ' Crea el DWord.
Debug.Print DWordToString(lDWord)
Stop
End Sub
Y magia, ahora sale en la ventana inmediato ABCD :), ¿qué es ABCD?, nada menos que los valores 65-66-67-68 en sus respectivos caracteres ASCII.
Creo que quedó claro el funcionamiento de la memoria, ahora ya viene la parte de las funciones que nos proporciona el SO para manejarla, pero antes, la gran diferencia entre ByVal y ByRef
4. Argumentos como referencia o como valorHay dos maneras de pasarle argumentos a las funciones, como valor o como referencia. En Visual Basic TODAS las variables se pasan como referencia a menos que se indique lo contrario usando ByVal, la palabra clave utilizada para pasar una variable como valor.
La diferencia es que al pasar una variable como referencia, lo que está pasando es el puntero a dicha variable. En cambio, pasando la variable como valor lo que se pasa es el valor o 'contenido' de la variable en ese instante.
Por ejemplo, tengo la variable lData y le asigno un valor cualquiera:
Ahora escribimos dos procedimientos, uno que acepte la variable como valor y otro como referencia:
Sub PassByRef(Var As Long)
Var = 654321
End Sub
Sub PassByVal(ByVal Var As Long)
Var = 654321
End Sub
Luego probamos ambas funciones con el siguiente procedimiento:
Sub TestPass()
Dim lDataByRef&, lDataByVal&
lDataByRef = 123456
lDataByVal = 123456
Debug.Print "El valor actual de lDataByRef es " & lDataByRef
Debug.Print "El valor actual de lDataByVal es " & lDataByVal
Call PassByRef(lDataByRef)
Call PassByVal(lDataByVal)
Debug.Print "El valor de lDataByRef es ahora " & lDataByRef
Debug.Print "El valor de lDataByVal es ahora " & lDataByVal
End Sub
Y sólo con mirar los outputs se dan cuenta lo que pasa. Cuando paso la variable como valor, lo que sucede es que se copia el contenido de la misma a otra parte de la memoria distinta de la dirección de esta, entonces el procedimiento modificará los datos en la dirección nueva de memoria, y al terminar (End Sub) VB borrará este 'espacio temporal' que usó para copiar el valor.
Como se puede ver, pasar un argumento como valor lleva un poco más de tiempo ya que tiene que asignar más memoria y copiar los datos, pero es muy necesario cuando se llama a las APIs ya que si pasamos una variable como referencia y a esta función se le ocurre escribir más allá del tamaño de la variable, hará que el programa produzca un error y se cierre porque vaya a saber qué es lo que modificó.
El caso de los datos String es algo particular, porque no se comportan como las variables numéricas. Las variables String están compuestas de dos partes, la variable en sí y los datos de la variable.
Como todos sabrán las variables String ocupan 4 bytes, esto se debe a que en realidad es una dirección de memoria donde están los datos de la misma. Por ejemplo, supongamos que declaro la variable sData y le asigno un valor:
Dim sData$
sData = "Hola mundo"
En este ejemplo hipotéticamente creeremos que la variable sData se encuentra en la dirección 8000, y los datos en 9000. Esto significa que en la dirección de memoria 8000 (el puntero a la variable sData), encontraremos el valor 9000 que indica la dirección de los datos en Unicode, ya que las cadenas de VB son todas Unicode (2 bytes por caracter)
5. Comportamiento de Strings en memoriaCreo que el tema de los Strings en VB merece un apartado especial, ya que es algo muy importante y es algo difícil de ver y entender.
Primero hay que destacar que existen dos tipos de String que usan las APIs y Visual Basic, LPSTR y BSTR.
Las cadenas LPSTR (String Pointer) son muy simples, se trata de una serie de caracteres terminados en un caracter nulo:
HOLA MUNDO\0 ' \0 = Caracter nulo
Se denomina puntero a la dirección de memoria del primer byte de un dato determinado. En el ejemplo anterior el puntero a esa cadena sería la dirección de 'H', o más bien del valor 72 (código ascii de 'H')Esto en Visual Basic es equivalente a una matriz de byte, ya que como dije antes no es más que una serie de valores de tipo Byte consecutivos en memoria.
En cambio Visual Basic utiliza las cadenas BSTR, que son un tipo de dato de automatización, adoptado de OLE Automation (Automatización OLE). Este tipo de String está diseñado para evitar problemas comunes como lo son los BOFs (Buffer Overflow), ya que debido a su estructura es imposible que se de esta situación en la asignación.
A diferencia de las LPSTR, las cadenas BSTR son SIEMPRE Unicode, esto significa que cada caracter ocupará 2 bytes (1 WORD). También hay que destacar que no terminan en un caracter nulo sino en dos caracteres nulos, como dije antes cada caracter es un WORD.
Otra característica especial es que los 4 bytes anteriores al inicio de la cadena especifican el tamaño de la misma, de esta manera el sistema no deberá escanear byte por byte buscando el final del String y el proceso se hará mucho más eficaz y rápido.
El siguiente esquema ilustra una cadena BSTR:
Len Cadena End
| |H O L A M U N D O|\0\0|
En el alfabeto latino y derivados sólo se usa el primer byte del WORD para almacenar el código de caracter, ya que los valores oscilan entre 0-255. En alfabetos orientales y demás tienen códigos de caracter muy superiores a 255 y por esta razón la necesidad de usar 2 bytes para guardar este valor.
Por un lado ya sabemos cómo se guardan las cadenas en memoria, ahora paso a explicar el funcionamiento de las variables String.
Primero hay que saber que existen 2 funciones no-documentadas de VB: VarPtr() y StrPtr()
VarPtr() devuelve el
puntero a una variable, cualquiera sea el tipo de variable.
StrPtr() devuelve el puntero a la cadena de una variable String.
Ahora, como ya comenté antes las variables String tienen dos partes, la variable en sí y los datos de la variable, que NO se encuentran en la misma dirección. Esto es mucho más sencillo explicarlo con un ejemplo. Vamos a utilizar el siguiente código de ejemplo:
Sub StrTest()
Dim sData$
sData = "Hola mundo"
Debug.Print Hex$(VarPtr(sData))
Debug.Print Hex$(StrPtr(sData))
Stop
End Sub
Ejecuten el procedimiento StrTest() y cuando la ejecución se detenga es donde nosotros comenzamos a experimentar con la memoria. Ahora sí, es determinante para la comprensión de este tema usar un visor de memoria/editor hexadecimal. Yo recomiendo el WinHex porque es el mejor desde mi punto de vista.
Ahora en la ventana Inmediato aparecieron dos direcciones, la primera es la dirección de la variable y la de abajo la dirección de los datos. Para comprobar que esto es cierto abrimos el WinHex, abrimos la memoria completa del proceso de VB (VB6.EXE) (Alt+F9 para ver la lista de procesos) y por último apretamos Alt+G para que aparezca el cuadro de "Ir a". Ahí vamos a colocar la primera dirección, en mi caso es 7FF540. El programa se posicionará en esa dirección de memoria y veremos algo como lo que muestra la siguiente imagen:
Si se fijan bien en esta dirección no hay ninguna cadena, pero el valor de 32 bits que está almacenado en la posición 7FF540 (VarPtr(sData)) es 64B718, o sea StrPtr(sData) que es la dirección de la cadena.
Ahora copiamos esta dirección que vemos y hacemos los mismo, Alt+G y vamos a la nueva dirección que nos manda la variable, o sea 64B718. En este momento una imagen vale más que mil palabras :)
En 64B718 es donde realmente están los datos de la variable sData.
Bueno, entonces resumiendo un poco (ya sé que repetí lo mismo muchas veces, pero es para que se entienda xD), si pasamos una variable String como ByRef estamos pasando la dirección de la variable, en el caso anterior sería 7FF540. En cambio al pasarla como ByVal estamos pasando la dirección de los datos, 64B718 en este ejemplo.
6. Funciones de manejo de memoriaNo voy a dar muchas vueltas, directamente voy a ir a lo que hace cada función y cómo se comporta en memoria.
6.1 CopyMemoryEsta es la función más antigua que implementa el sistema operativo pero sigue siendo muy funcional y vigente. El prototipo de esta función es el siguiente:
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
El uso de esta función es muy simple siempre y cuando se esté bien conciente de qué se está copiando y a dónde. Los parámetros que hay que pasarle son los siguientes:
Destination: Dirección de memoria a la cual se copiarán los datos.
Source: Dirección de memoria desde donde se copiarán los datos.
Length: Cantidad de bytes que la función leerá desde Source y copiará a Destination.
El gran problema a la hora de usar esta función es que hay que saber cuándo debemos pasar un parámetro como valor o como referencia. Si no leyeron todo el documento les recomiendo que lean '
Argumentos como referencia o como valor' primero.
Consideremos el siguiente ejemplo:
Sub CopyMem()
Dim lSourceData&, lDestData&
lSourceData = 123456
Call CopyMemory(lDestData, lSourceData, Len(lSourceData))
Debug.Print "Ahora lDestData es igual a " & lDestData
Stop
End Sub
Para analizar esto hay que tener en cuenta lo siguiente:
La palabra clave Any se utiliza para pasar el puntero (vease definición de puntero en la sección anterior) a CUALQUIER TIPO de variable. Pasarle una variable a una función con argumentos declarados como Any, es análogo a usar ByVal VarPtr(Variable). Por ejemplo:
Sub CopyMem()
Dim lSourceData&, lDestData&
lSourceData = 123456
Call CopyMemory(ByVal VarPtr(lDestData), ByVal VarPtr(lSourceData), Len(lSourceData))
Debug.Print "Ahora lDestData es igual a " & lDestData
Stop
End Sub
¿Por qué?. En mi caso particular la dirección de lSourceData es 7FF540, y de lDestData es 7FF53C.
Cuando llamamos a VarPtr(), como ya notaron anteriormente, nos devuelve 7FF540 para lSourceData y 7FF53C para lDestData. Mirando el primer ejemplo, cuando se pasan las variables como Any y por referencia , lo que Visual Basic le pasa a la función CopyMemory son esos valores que devuelve VarPtr().
Si pasamos la variable como ByVal, lo que hace VB es pasarle a la función CopyMemory los datos de la misma. Por ejemplo:
Sub CopyMem()
Dim lSourceData&, lDestData&
lSourceData = 123456
Call CopyMemory(lDestData, ByVal lSourceData, Len(lSourceData))
End Sub
Lo que le estoy diciendo ahí es que copie a 7FF53C lo que hay en la dirección 123456, ya que en este caso se le pasa el valor de lSourceData y CopyMemory SIEMPRE toma los argumentos como direcciones de memoria.
Volviendo al ejemplo de ByVal VarPtr(Variable), lo que le estoy indicando a VB es que le pase el valor que devuelva VarPtr, o sea 7FF540 entonces CopyMemory leerá en el lugar correcto. Eso sería equivalente a lo siguiente:
Sub CopyMem()
Dim lSourceData&, lDestData&
lSourceData = VarPtr(lSourceData)
lDestData = VarPtr(lDestData)
Call CopyMemory(ByVal lDestData, lSourceData, Len(lSourceData))
Debug.Print "Ahora lDestData es igual a " & lDestData
Stop
End Sub
Ya que ahora, cada variable va a contener la dirección de sí misma. Esto es cuestión de probar y ver lo que pasa, por más que yo se los cuente si no lo ven será difícil de asimilar.
En el caso de una tipo definido por el usuario (TDU), es exactamente lo mismo. Lo que hay que saber, es que cada registro de los TDU se encuentran consecutivos en memoria. Veamos el siguiente ejemplo:
Type SomeData
Size As Integer
Registro1 As Long
Registro2 As Long
Registro3 As Currency
Registro4 As String
End Type
En memoria estaría dispuesto de la siguiente manera:
2 4 4 8 4
Posición más baja|..|....|....|........|....|Posición más alta
Esta estructura ocuparía 22 bytes de memoria, ya que son 2 Long (8 bytes) más un Currency (8 bytes) más un Integer (2 bytes) más un String (4 bytes).
Vamos a creer que el puntero a la estructura está en 1000 (el puntero es el primer byte del primer registro, o sea el primer byte del campo Size).
Offset Campo
1000 Size
1002 Registro1 ' VarPtr(Size) + 2
1006 Registro2 ' VarPtr(Registro1) + 4
1010 Registro3 ' VarPtr(Registro2) + 4
1018 Registro4 ' VarPtr(Registro3) + 8
Con esto creo que está concluída la explicación del uso de CopyMemory() con variables no-String.
6.2 CopyMemory y StringsPara este tema prefiero escribir un apartado porque si bien es similar, es mucho más importante porque puede llegar a provocar un error general del programa y que se cierre.
Antes de seguir recomiendo que lean
5. Comportamiento de Strings en memoria, porque no pienso volver a repetir todo, sólo un repaso muy por arriba.
Bueno ahora sí empiezo. Para copiar los datos de una variable String hay que tener en cuenta dos puntos claves:
1. SIEMPRE y sin excepciones hay que usar ByVal.
2. Antes de llamar a CopyMemory la variable de destino tiene que ser inicializada como mínimo con la misma cantidad de datos que la variable desde donde se copia (Source).
Estos dos items los voy a explicar con un ejemplo CORRECTO, luego voy a mostrar la forma errónea.
Sub CopyMemStr()
Dim sSrcData$, sDestData$
sSrcData = "Hola mundo"
sDestData = String$(Len(sSrcData), 0)
Call CopyMemory(ByVal sDestData, ByVal sSrcData, LenB(sSrcData))
Debug.Print sSrcData, sDestData
Stop
End Sub
Este código funcionará correctamente ya que lo que se está haciendo es copiar el contenido de una variable a la otra. A continuación muestro los datos que hay en memoria para la variable sSrcData luego de establecer el ejecutarse la linea
sSrcData = "Hola mundo":
ACLARACIÓN: Para ver bien los datos del dumpeo de memoria copienlos al bloc de notas y deshabiliten el ajuste de linea.Datos de sSrcData
0062D920 14 00 00 00 48 00 6F 00 6C 00 61 00 20 00 6D 00 75 00 6E 00 64 00 6F 00 ....H.o.l.a. .m.u.n.d.o.
0062D938 00 00 72 00 79 00 00 00 CC 00 00 A0 00 00 00 00 EC 0A 67 00 FF FF FF FF ..r.y...Ì.. ....ì.g.ÿÿÿÿ
0062D950 FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FC DE 62 00 ÿÿÿÿ................üÞb.
0062D968 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 00 00 00 00 00 C8 06 67 00 ....................È.g.
0062D980 FF FF FF FF 6C 13 67 00 00 00 00 00 0C 65 61 00 00 00 00 00 14 DF 62 00 ÿÿÿÿl.g......ea......ßb.
0062D998 01 00 00 00 AC A1 63 00 ....¬¡c.
El puntero a la cadena es 62D924, o sea la dirección de 'H', y la cadena ocupa en memoria 20 bytes, o sea LenB(sSrcData), ya que como pueden observar se encuentra en Unicode.
Lo segundo que se debe hacer es reservar la MISMA cantidad de espacio en sDestData para poder copiar los datos de sSrcData, eso es lo que hacemos con la función String():
sDestData = String$(Len(sSrcData), 0)
Por empezar, llenamos la cadena sDestData con caracteres nulos pero sólo es por una cuestión de comodidad, pero puede ser cualquier caracter que queramos. Una vez que asignamos espacio los datos de sDestData se verían así en memoria:
Datos de sDestData antes de llamar a CopyMemory
0066E048 14 00 00 00 ....
0066E060 48 00 6F 00 6C 00 61 00 20 00 6D 00 75 00 6E 00 64 00 6F 00 00 00 02 00 ........................
0066E078 0E 00 18 02 78 00 00 A0 18 E0 66 00 80 1E 67 00 B0 2A 67 00 30 2B 67 00 ....x.. .àf.€.g.°*g.0+g.
0066E090 9C 2B 67 00 AC 2B 67 00 BC 2B 67 00 A0 29 67 00 5C 37 67 00 70 37 67 00 œ+g.¬+g.¼+g. )g.\7g.p7g.
0066E0A8 80 37 67 00 8C 37 67 00 98 37 67 00 A8 37 67 00 B4 37 67 00 C4 37 67 00 €7g.Œ7g.˜7g.¨7g.´7g.Ä7g.
0066E0C0 D8 37 67 00 E8 37 67 00 F8 37 67 00 08 38 67 00 18 38 67 00 28 38 67 00 Ø7g.è7g.ø7g..8g..8g.(8g.
0066E0D8 38 38 67 00 88g.
Desde la posición 66E060 (StrPtr(sDesdData)) vemos que ahora la cadena sDestData está inicializada, entonces ya estamos preparados para llamar a CopyMemory:
Call CopyMemory(ByVal sDestData, ByVal sSrcData, LenB(sSrcData))
Lo que le estamos diciendo es que copie 20 bytes desde 62D924 (el puntero a los datos de sSrcData) a 66E060 (el puntero a los datos de sDestData), por lo tanto quedarían exactamente igual:
Datos de sSrcData
0062D920 14 00 00 00 48 00 6F 00 6C 00 61 00 20 00 6D 00 75 00 6E 00 64 00 6F 00 ....H.o.l.a. .m.u.n.d.o.
0062D938 00 00 72 00 79 00 00 00 CC 00 00 A0 00 00 00 00 EC 0A 67 00 FF FF FF FF ..r.y...Ì.. ....ì.g.ÿÿÿÿ
0062D950 FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FC DE 62 00 ÿÿÿÿ................üÞb.
0062D968 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 00 00 00 00 00 C8 06 67 00 ....................È.g.
0062D980 FF FF FF FF 6C 13 67 00 00 00 00 00 0C 65 61 00 00 00 00 00 14 DF 62 00 ÿÿÿÿl.g......ea......ßb.
0062D998 01 00 00 00 AC A1 63 00 ....¬¡c.
Datos de sDestData luego de llamar a CopyMemory
0066E048 14 00 00 00 ....
0066E060 48 00 6F 00 6C 00 61 00 20 00 6D 00 75 00 6E 00 64 00 6F 00 00 00 02 00 H.o.l.a. .m.u.n.d.o.....
0066E078 0E 00 18 02 78 00 00 A0 18 E0 66 00 80 1E 67 00 B0 2A 67 00 30 2B 67 00 ....x.. .àf.€.g.°*g.0+g.
0066E090 9C 2B 67 00 AC 2B 67 00 BC 2B 67 00 A0 29 67 00 5C 37 67 00 70 37 67 00 œ+g.¬+g.¼+g. )g.\7g.p7g.
0066E0A8 80 37 67 00 8C 37 67 00 98 37 67 00 A8 37 67 00 B4 37 67 00 C4 37 67 00 €7g.Œ7g.˜7g.¨7g.´7g.Ä7g.
0066E0C0 D8 37 67 00 E8 37 67 00 F8 37 67 00 08 38 67 00 18 38 67 00 28 38 67 00 Ø7g.è7g.ø7g..8g..8g.(8g.
0066E0D8 38 38 67 00 88g.
Como vemos los datos son exactamente iguales. Acá lo importante es inicializar la cadena de destino, porque sino estaríamos sobreescribiendo los datos que siguen y vaya a saber qué modificamos, por eso produce un error de página no válida y se cierra el programa.
Si no pasamos los datos como ByVal lo que pasaría es que modificaría la variable, no los datos, y sobreescribiría datos que no queremos por lo que se cerrará el programa.
Variable sSrcData (007FF540) y sDestData (7FF53C)
007FF530 00 00 00 00 00 00 00 00 00 00 00 00 60 E0 66 00 24 D9 62 00 D4 F5 7F 00 ............`àf.$Ùb.Ôõ.
007FF548 81 1B 67 00 04 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 6B 19 C0 0F .g.................k.À.
Los datos de la variable sDestData son '60 E0 66 00' y los de sSrcData son '24 D9 62 00', que si usamos las funciones MakeWord y MakeDWord como lo expliqué antes veremos que obtendremos las direcciones de los datos. La cuestión es que si no usamos ByVal le estamos diciendo que copie 20 bytes desde 007FF540 (sSrcData) a 7FF53C (sDestData), o sea que reemplazaría '60 E0 66 00' por '24 D9 62 00 D4 F5 7F 00 81 1B 67 00 04 00 00 00 00 00 00 00 02' y sobreescribiría en memoria a sSrcData que está justo después de sDestData, entonces ya no se puede predecir lo que pasará porque no sabemos qué datos arruinamos, y en general se jode el programa :P.
No sé si podría explicarlo más claro, cualquier consulta en este mismo post, para comprender esto hay que practicar y ver lo que pasa.
6.3 Asignar y liberar memoriaEl uso de estas funciones es muy sencillo, pero antes de hablar de ellas voy a describir un poco como se conforma la memoria.
6.3.1 Distribución de la memoria virtual El espacio de memoria virtual está dividido en particiones:
Windows NT Services, Edición Empresarial | La partición de 3 GB en la memoria baja (0x00000000 a 0xBFFFFFFF [3221225471]) está disponible para los procesos, y una partición de 1 GB en la memoria alta (0xC0000000 [3221225472] a 0xFFFFFFFF [4294967295]) está reservada para el sistema |
Window NT | La partición de 2 GB en la memoria baja (0x00000000 a 0x7FFFFFFF [2147483648]) está disponible para los procesos y la partición de 2 GB de en la memoria alta (0x80000000 (2147483648) a 0xFFFFFFFF [4294967295]) está reservado para el sistema. |
Windows 95 y Windows 98 | La partición de 4 GB en la memoria baja (0x00000000 a 0x00000FFF [4095]) es usada para compatibilidad con MS-DOS y Windows de 16 bits, la siguiente partición aproximadamente de 2 GB (0x00400000 [4194304] a 0x7FFFFFFF [2147483647] está disponible por los procesos para uso privado, la siguiente partición de 1 GB (0x80000000 [2147483648] a 0xBFFFFFFF [3221225471]) es compartida por todos los procesos de Win32, y la partición de 1 GB en memoria alta (0xC0000000 [3221225472] a 0xFFFFFFFF [4294967295]) es usada por el sistema. |
En general los procesos se cargan en la dirección 0x400000 ya que es compatible con todas las versiones de Windows.
6.3.2 Espacio de Direcciones Virtuales y Almacenamiento Físico El espacio de direcciones virtuales es la memoria que hay disponible para cada proceso. Se llama así ya que al usar un archivo en el disco para extender la memoria física (archivo de paginación o swap) el sistema puede crear un "mapa" de memoria idéntico para cada proceso. Esto significa que cada proceso virtualmente tiene acceso a TODA la memoria física e incluso más todavía, y además cada proceso puede escribir en la misma dirección debido a que es virtual, por lo tanto no sobreescribiría lo de otro. En resumen, Windows crea una especie de memoria RAM en el disco rígido para cada proceso.
El espacio de direcciones virtuales para cada proceso es mucho mayor que el total de memoria física para todos los procesos. Como dije antes, para incrementar el tamaño del almacenamiento físico, el sistema usa el disco para almacenamiento adicional.
La cantidad total de almacenamiento disponible para todos los procesos que se están ejecutando es la suma de la memoria física y el espacio libre en el disco disponible para el archivo de paginación.
El almacenamiento físico y el espacio de direcciones virtuales para cada proceso están organizados en páginas, unidades de memoria, cuyo tamaño depende del microprocesador. Por ejemplo, en computadoras x86 el tamaño de página es de 4 KB (4096 bytes).
Pata maximizar la flexibilidad en la memoria principal, el sistema puede mover páginas de memoria física a y desde el archivo de paginación (swap) en el disco. Cuando la página es movida o modificada en la memoria física, el sistema actualiza el mapa de páginas de los procesos afectados para que los datos de la memoria física y del archivo de paginación sean iguales. Cuando el sistema necesita espacio en la memoria física, mueve las páginas recientemente usadas de memoria física a la swap. La manipulación de la memoria física por el sistema es
completamente transparente para las aplicaciones, las cuales operan sólo en sus espacios de memoria virtual.
6.3.3 Estado de las páginas Las páginas del espacio de direcciones virtuales de un proceso pueden estar en uno de los siguientes estados:
Libre | Una página libre no está actualmente accesible, pero está disponible para ser encargada o reservada |
Reservada | Una página reservada es un bloque en el espacio de direcciones virtuales de un proceso que ha sido fijado para usos futuros. El proceso no puede acceder a una página reservada, y no hay almacenamiento físico asociado con esta, o sea que no se aumenta el tamaño de la swap. Una página reservada reserva un rango de direcciones virtuales que no pueden ser usadas subsecuentemente por otras funciones de asignación. Un proceso puede usar la función VirtualAlloc para reservar páginas de su espacio de memoria y luego encargar las páginas reservadas. Luego se puede usar VirtualFree para descargarlas. |
Encargada | Una página encargada es una para la cual ha sido asignado almacenamiento físico (en la memoria física o en el disco). Puede ser protegida para permitir o no el acceso o el acceso sólo-lectura, o puede tener acceso para lectura y escritura. Un proceso puede usar la función VirtualAlloc para asignar páginas encargadas. Las funciones GlobalAlloc y LocalAlloc asignan páginas encargadas con acceso para lectura-escritura. Una página encargada asignada por VirtualAlloc puede ser "descargada" por la función VirtualFree, con la cual se descargan las páginas almacenadas y cambia el estado de la página a reservada. |
6.3.4 Alcance de la memoria asignada Toda la memoria que un proceso asigna usando las funciones de asignación de memoria (HeapAlloc, VirtualAlloc, GlobalAlloc, LocalAlloc) son accesibles sólo para dicho proceso. Sin embargo, la memoria asignada por una DLL está asignada en el espacio de direcciones del proceso que ha llamado a la DLL y no está accesible por otros procesos usando la misma DLL. Para crear memoria compartida, hay que usar el mapeado de archivos (objetos file-mapping) que se explican más adelante.
Esto significa que creando una DLL podemos tener acceso a la memoria cualquier proceso, y esto se usa mucho como método de intección. Por ejemplo, si un proceso carga el archivo Injection.DLL y dentro del código de Injection.DLL se llama a VirtualAlloc, va a asignar memoria en el proceso que cargó esta librería. Otra particularidad es que las funciones GetCurrentProcess(), GetCurrentThread(), GetCurrentProcessId() y GetCurrentThreadId() también devuelven los datos del proceso que cargó a la DLL.