atkayla
En un intento de ver esto, escribí este código simple donde simplemente creé variables de diferentes tipos y las pasé a una función por valor, por referencia y por puntero:
int i = 1;
char c="a";
int* p = &i;
float f = 1.1;
TestClass tc; // has 2 private data members: int i = 1 and int j = 2
los cuerpos de las funciones se dejaron en blanco porque solo estoy mirando cómo se pasan los parámetros.
passByValue(i, c, p, f, tc);
passByReference(i, c, p, f, tc);
passByPointer(&i, &c, &p, &f, &tc);
Quería ver cómo esto es diferente para una matriz y también cómo se accede a los parámetros.
int numbers[] = {1, 2, 3};
passArray(numbers);
asamblea:
passByValue(i, c, p, f, tc)
mov EAX, DWORD PTR [EBP - 16]
mov DL, BYTE PTR [EBP - 17]
mov ECX, DWORD PTR [EBP - 24]
movss XMM0, DWORD PTR [EBP - 28]
mov ESI, DWORD PTR [EBP - 40]
mov DWORD PTR [EBP - 48], ESI
mov ESI, DWORD PTR [EBP - 36]
mov DWORD PTR [EBP - 44], ESI
lea ESI, DWORD PTR [EBP - 48]
mov DWORD PTR [ESP], EAX
movsx EAX, DL
mov DWORD PTR [ESP + 4], EAX
mov DWORD PTR [ESP + 8], ECX
movss DWORD PTR [ESP + 12], XMM0
mov EAX, DWORD PTR [ESI]
mov DWORD PTR [ESP + 16], EAX
mov EAX, DWORD PTR [ESI + 4]
mov DWORD PTR [ESP + 20], EAX
call _Z11passByValueicPif9TestClass
passByReference(i, c, p, f, tc)
lea EAX, DWORD PTR [EBP - 16]
lea ECX, DWORD PTR [EBP - 17]
lea ESI, DWORD PTR [EBP - 24]
lea EDI, DWORD PTR [EBP - 28]
lea EBX, DWORD PTR [EBP - 40]
mov DWORD PTR [ESP], EAX
mov DWORD PTR [ESP + 4], ECX
mov DWORD PTR [ESP + 8], ESI
mov DWORD PTR [ESP + 12], EDI
mov DWORD PTR [ESP + 16], EBX
call _Z15passByReferenceRiRcRPiRfR9TestClass
passByPointer(&i, &c, &p, &f, &tc)
lea EAX, DWORD PTR [EBP - 16]
lea ECX, DWORD PTR [EBP - 17]
lea ESI, DWORD PTR [EBP - 24]
lea EDI, DWORD PTR [EBP - 28]
lea EBX, DWORD PTR [EBP - 40]
mov DWORD PTR [ESP], EAX
mov DWORD PTR [ESP + 4], ECX
mov DWORD PTR [ESP + 8], ESI
mov DWORD PTR [ESP + 12], EDI
mov DWORD PTR [ESP + 16], EBX
call _Z13passByPointerPiPcPS_PfP9TestClass
passArray(numbers)
mov EAX, .L_ZZ4mainE7numbers
mov DWORD PTR [EBP - 60], EAX
mov EAX, .L_ZZ4mainE7numbers+4
mov DWORD PTR [EBP - 56], EAX
mov EAX, .L_ZZ4mainE7numbers+8
mov DWORD PTR [EBP - 52], EAX
lea EAX, DWORD PTR [EBP - 60]
mov DWORD PTR [ESP], EAX
call _Z9passArrayPi
// parameter access
push EAX
mov EAX, DWORD PTR [ESP + 8]
mov DWORD PTR [ESP], EAX
pop EAX
¡Supongo que estoy mirando el ensamblaje correcto relacionado con el paso de parámetros porque hay llamadas al final de cada uno!
Pero debido a mi conocimiento muy limitado de ensamblaje, no puedo decir qué está pasando aquí. Aprendí sobre la convención ccall, así que asumo que está sucediendo algo que tiene que ver con preservar los registros guardados por la persona que llama y luego empujar los parámetros a la pila. Debido a esto, espero ver cosas cargadas en registros y “empujar” en todas partes, pero no tengo idea de qué está pasando con el mov
arena lea
s. Además, no sé qué DWORD PTR
es.
Solo he aprendido sobre registros: eax, ebx, ecx, edx, esi, edi, esp
y ebp
así que viendo algo como XMM0
o DL
simplemente me confunde también. Supongo que tiene sentido ver lea
cuando se trata de pasar por referencia/puntero porque usan direcciones de memoria, pero en realidad no puedo decir qué está pasando. Cuando se trata de pasar por valor, parece que hay muchas instrucciones, por lo que esto podría tener que ver con copiar el valor en los registros. No tengo idea cuando se trata de cómo se pasan y se accede a las matrices como parámetros.
Si alguien pudiera explicarme la idea general de lo que está pasando con cada bloque de ensamblaje, se lo agradecería mucho.
El uso de registros de la CPU para pasar argumentos es más rápido que el uso de la memoria, es decir, la pila. Sin embargo, hay una cantidad limitada de registros en la CPU (especialmente en las CPU compatibles con x86), por lo que cuando una función tiene muchos parámetros, se usa la pila en lugar de los registros de la CPU. En su caso, hay 5 argumentos de función, por lo que el compilador usa la pila para los argumentos en lugar de los registros.
En principio, los compiladores pueden usar push
instrucciones para empujar argumentos para apilar antes de real call
para funcionar, pero muchos compiladores (incl. gnu c++) usan mov
para empujar argumentos a la pila. Esta forma es conveniente ya que no cambia el registro ESP (parte superior de la pila) en la parte del código que llama a la función.
En caso de passByValue(i, c, p, f, tc)
los valores de los argumentos se colocan en la pila. Puedes ver muchos mov
instrucción desde una ubicación de memoria a un registro y desde el registro a una ubicación apropiada de la pila. La razón de esto es que el ensamblaje x86 prohíbe el movimiento directo de una ubicación de memoria a otra (la excepción es movs
que mueve valores de una matriz (o cadena como desee) a otra).
En caso de passByReference(i, c, p, f, tc)
puedes ver muchas instrucciones de 5 lea que copian direcciones de argumentos a los registros de la CPU, y estos valores de los registros se mueven a la pila.
El caso de passByPointer(&i, &c, &p, &f, &tc)
es parecido a passByValue(i, c, p, f, tc)
. Internamente, en el nivel de ensamblaje, el paso por referencia usa punteros, mientras que en el nivel superior, C++, un programador no necesita usar explícitamente el &
y *
operadores sobre referencias.
Después de mover los parámetros a la pila call
se emite, lo que empuja el puntero de instrucción EIP
para apilar antes de transferir la ejecución del programa a la subrutina. Todo moves
de los parámetros a la cuenta de pila para el próximo EIP
en la pila después de la call
instrucción.
-
¡Muy útil! ¡Gracias! ¿Podría explicar la parte que trata con las matrices? Veo tres mov EAX separados, por lo que parece que tiene que ver con cómo hice una matriz de tres elementos. También hay una pista, ¿podría ser esta la dirección base?
– atkayla
8 de noviembre de 2013 a las 13:10
-
@TidusSmith Las matrices siempre se pasan por referencia (puntero) en C.
lea EAX, DWORD PTR [EBP - 60]
mueve la dirección de la matriz a la pila. Elmov
instrucciones justo antes de lalea
la instrucción es una coincidencia como probablemente pusisteint numbers[] = {1,2,3};
justo antespassArray(numbers)
. Sinumbers[]
es una variable local, entonces la matriz se llena de uno en uno (valores 1,2,3 en caso de que fnumber[]
), que son realizados pormov
s en la salida del ensamblado. Esto no significa que los valores de la matriz se pasan por valores. Si una matriz es una variable global, los valores de la matriz están cableados en la sección .data.– Ígor Popov
8 de noviembre de 2013 a las 14:51
gran lobo
Hay demasiado en su ejemplo anterior para diseccionarlos todos. En su lugar, solo iré passByValue
ya que eso parece ser lo más interesante. Después, deberías poder averiguar el resto.
Primero, algunos puntos importantes a tener en cuenta al estudiar el desensamblado para no perderse por completo en el mar del código:
- No hay instrucciones para copiar datos directamente de una ubicación de memoria a otra ubicación de memoria. p.ej.
mov [ebp - 44], [ebp - 36]
es no legal instrucción. Se necesita un registro intermedio para almacenar los datos primero y luego copiarlos en el destino de la memoria. - Operador de soporte
[]
junto con unmov
medios para acceder a datos desde una dirección de memoria computada. Esto es análogo a eliminar la referencia de un puntero en C/C++. - Cuando veas
lea x, [y]
eso por lo general significa calcular la dirección de y y guardar en X. Esto es análogo a tomar la dirección de una variable en C/C++. - Los datos y los objetos que deben copiarse pero que son demasiado grandes para caber en un registro se copian en la pila por partes. IOW, copiará una palabra de máquina nativa a la vez hasta que se copien todos los bytes que representan el objeto/datos. Por lo general, eso significa 4 u 8 bytes en los procesadores modernos.
- El compilador típicamente intercalar instrucciones juntas para mantener ocupada la canalización del procesador y minimizar las paradas. Bueno para la eficiencia del código pero malo si estás tratando de entender el desmontaje.
Con lo anterior en mente aquí está la llamada a passByValue
función reorganizada un poco para que sea más comprensible:
.define arg1 esp
.define arg2 esp + 4
.define arg3 esp + 8
.define arg4 esp + 12
.define arg5.1 esp + 16
.define arg5.2 esp + 20
; copy first parameter
mov EAX, [EBP - 16]
mov [arg1], EAX
; copy second parameter
mov DL, [EBP - 17]
movsx EAX, DL
mov [arg2], EAX
; copy third
mov ECX, [EBP - 24]
mov [arg3], ECX
; copy fourth
movss XMM0, DWORD PTR [EBP - 28]
movss DWORD PTR [arg4], XMM0
; intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI
;copy fifth
lea ESI, [EBP - 48]
mov EAX, [ESI]
mov [arg5.1], EAX
mov EAX, [ESI + 4]
mov [arg5.2], EAX
call passByValue(int, char, int*, float, TestClass)
El código anterior está desmantelado y la mezcla de instrucciones se ha deshecho para dejar en claro lo que realmente está sucediendo, pero aún es necesario explicar algo. Primero, el char es signed
y tiene un tamaño de un solo byte. Las instrucciones aquí:
; copy second parameter
mov DL, [EBP - 17]
movsx EAX, DL
mov [arg2], EAX
lee un byte de [ebp - 17]
(en algún lugar de la pila) y lo almacena en el primer byte inferior de edx
. Luego, ese byte se copia en eax
usando el movimiento de signo extendido. El valor completo de 32 bits en eax
finalmente se copia en la pila que passByValue
puede acceder. Ver diseño de registro si necesita más detalles.
El cuarto argumento:
movss XMM0, DWORD PTR [EBP - 28]
movss DWORD PTR [arg4], XMM0
Utiliza el SSE movss
instrucción para copiar el valor de punto flotante de la pila en un xmm0
registro. En resumen, las instrucciones SSE le permiten realizar la misma operación en varios datos simultáneamente, pero aquí el compilador lo usa como un almacenamiento intermedio para copiar valores de punto flotante en la pila.
El último argumento:
; copy intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI
corresponde a la TestClass
. Aparentemente, esta clase tiene un tamaño de 8 bytes ubicada en la pila de [ebp - 40]
a [ebp - 33]
. La clase aquí se copia 4 bytes a la vez ya que el objeto no puede caber en un solo registro.
Así es como se ve aproximadamente la pila antes de call passByValue
:
lower addr esp => int:arg1 <--.
esp + 4 char:arg2 |
esp + 8 int*:arg3 | copies passed
esp + 12 float:arg4 | to 'passByValue'
esp + 16 TestClass:arg5.1 |
esp + 20 TestClass:arg5.2 <--.
...
...
ebp - 48 TestClass:arg5.1 <-- intermediate copy of
ebp - 44 TestClass:arg5.2 <-- TestClass?
ebp - 40 original TestClass:arg5.1
ebp - 36 original TestClass:arg5.2
...
ebp - 28 original arg4 <--.
ebp - 24 original arg3 | original (local?) variables
ebp - 20 original arg2 | from calling function
ebp - 16 original arg1 <--.
...
higher addr ebp prev frame
lo que buscas son Convenciones de llamadas ABI. Las diferentes plataformas tienen diferentes convenciones. por ejemplo, Windows en x86-64 tiene diferentes convenciones que Unix/Linux en x86-64.
http://www.agner.org/optimizar/ tiene un documento de convenciones de llamadas que detalla las distintas para x86/amd64.
Puede escribir código en ASM que haga lo que quiera, pero si quiere llamar a otras funciones y ser llamado por ellas, entonces pase parámetros/valores devueltos de acuerdo con la ABI.
Podría ser útil crear una función de ayuda solo para uso interno que no use el ABI estándar, sino que use valores en los registros en los que los asigna la función de llamada. Esto es especialmente. probablemente si está escribiendo el programa principal en algo que no sea ASM, con solo una pequeña parte en ASM. Luego, la parte asm solo debe preocuparse por ser portátil a sistemas con diferentes ABI para ser llamado desde el programa principal, no por sus propios componentes internos.
DWORD PTR
(o simplementeDWORD
en la sintaxis de NASM) significa que le está diciendo al ensamblador que el operando de memoria es unDWORD
. En casos comomov DWORD PTR [ESP], EAX
es redundante porque el ensamblador podría determinar el tamaño del operando de memoria sin ambigüedades en función del hecho de queEAX
es de 32 bits Pero es relevante en casos comomov DWORD PTR [ESP],0
.– Miguel
8 de noviembre de 2013 a las 12:24
Es difícil ver una pregunta aquí que no pueda responderse leyendo un libro sobre programación en lenguaje ensamblador. Esperar que un usuario de SO escriba ese libro para usted en una respuesta es bastante irrazonable y no tan útil, no hay suficiente espacio para un libro.
-Hans Passant
8 de noviembre de 2013 a las 12:37
Si antepone las declaraciones de su función con
extern "C"
entonces los nombres de los símbolos no se estropearán tanto en el desmontaje. Esto hará que sea más fácil de reconocer y localizar.– gran lobo
8 de noviembre de 2013 a las 12:42