registros lenguaje instrucciones ensamblador ejemplos ejemplo basicas banderas assembly stack

assembly - lenguaje - offset ensamblador



¿Cómo funciona la pila en lenguaje ensamblador? (17)

¿Qué es Stack? Una pila es un tipo de estructura de datos: un medio para almacenar información en una computadora. Cuando se ingresa un nuevo objeto en una pila, se coloca encima de todos los objetos previamente ingresados. En otras palabras, la estructura de datos de la pila es como una pila de tarjetas, documentos, correos de tarjetas de crédito o cualquier otro objeto del mundo real que se te ocurra. Al eliminar un objeto de una pila, el que se encuentra en la parte superior se elimina primero. Este método se conoce como LIFO (último en entrar, primero en salir).

El término "pila" también puede ser corto para una pila de protocolos de red. En la creación de redes, las conexiones entre computadoras se realizan a través de una serie de conexiones más pequeñas. Estas conexiones, o capas, actúan como la estructura de datos de la pila, en el sentido de que se construyen y eliminan de la misma manera.

Actualmente estoy tratando de entender cómo funciona la pila, así que decidí enseñarme un poco de lenguaje ensamblador , estoy usando este libro:

http://savannah.nongnu.org/projects/pgubook/

Estoy usando Gas y haciendo mi desarrollo en Linux Mint .

Estoy un poco confundido por algo:

Por lo que yo sé, una pila es simplemente una estructura de datos. Así que supuse que si estaba codificando en ensamblaje tendría que implementar la pila yo mismo. Sin embargo, este no parece ser el caso ya que hay comandos como

pushl popl

Entonces, al codificar en ensamblaje para la arquitectura x86 y usar la sintaxis de Gas, ¿la pila solo es una estructura de datos que ya está implementada? ¿O es realmente implementado en el nivel de hardware? ¿O es otra cosa? ¿También la mayoría de los lenguajes de ensamblaje para otros conjuntos de chips ya tienen la pila implementada?

Sé que es una pregunta tonta, pero estoy bastante confundido por esto.


(He hecho un gist de todo el código en esta respuesta en caso de que quiera jugar con él)

Solo he hecho la mayoría de las cosas básicas en ASM durante mi curso de CS101 en 2003. Y nunca lo había "entendido" cómo funcionaba ASM y Stack hasta que me he dado cuenta de que todo es básicamente como programar en C o C ++ ... pero sin variables locales, parámetros y funciones. Probablemente aún no parezca fácil :) Déjame mostrarte (para x86 asm con sintaxis Intel ).

1. ¿Cuál es la pila

Stack es un fragmento contiguo de memoria asignado para cada hilo cuando se inicia. Puedes almacenar allí lo que quieras. En lenguaje de C ++ ( fragmento de código n. ° 1 ):

const int STACK_CAPACITY = 1000; thread_local int stack[STACK_CAPACITY];

2. Parte superior e inferior de la pila

En principio, puede almacenar valores en celdas aleatorias de la matriz de stack ( fragmento # 2.1 ):

cin >> stack[333]; cin >> stack[517]; stack[555] = stack[333] + stack[517];

Pero imagine lo difícil que sería recordar qué celdas de la stack ya están en uso y cuáles son "libres". Es por eso que almacenamos nuevos valores en la pila uno al lado del otro.

Una cosa rara de la pila de (x86) asm es que agregas cosas allí comenzando con el último índice y avanzas a índices más bajos: pila [999], luego pila [998] y así sucesivamente ( fragmento # 2.2 ):

cin >> stack[999]; cin >> stack[998]; stack[997] = stack[999] + stack[998];

Y aún (coution, vas a estar confundido ahora) el nombre "oficial" para stack[999] está en la parte inferior de la pila .
La última celda utilizada ( stack[997] en el ejemplo anterior) se llama parte superior de la pila (consulte Dónde está la parte superior de la pila en x86 ).

3. Puntero de pila (SP)

La pila no es lo único visible en todas partes en su código asm. También puede manipular registros de CPU (consulte Registros de propósito general ). Son realmente como variables globales:

int AX, BX, SP, BP, ...; int main(){...}

Hay un registro de CPU dedicado (SP) para realizar un seguimiento del último elemento agregado a la pila. Como su nombre lo sugiere es, bueno, un puntero (contiene una dirección de memoria como 0xAAAABBCC). Pero a los fines de esta publicación lo usaré como índice.

Al inicio del subproceso SP == STACK_CAPACITY y luego lo disminuyes cuando surge la necesidad. La regla es que no puede escribir en celdas de pila más allá de la pila y ningún índice menor que SP no es válido, por lo que primero disminuye el SP y luego escribe un valor en la celda recién asignada.

Cuando sepa que va a agregar varios valores en la pila en una fila, puede reservar espacio para todos ellos por adelantado ( fragmento # 3 ):

SP -= 3; cin >> stack[999]; cin >> stack[998]; stack[997] = stack[999] + stack[998];

Nota. Ahora puedes ver por qué "asignar" en la pila es tan rápido. En realidad, no se asigna nada (como en las palabras clave new o malloc ), es solo una disminución de un solo entero.

4. Deshacerse de las variables locales

Tomemos esta función simplista ( fragmento # 4.1 ):

int triple(int a) { int result = a * 3; return result; }

y reescribirlo sin variable local ( fragmento # 4.2 ):

int triple_noLocals(int a) { SP -= 1; // move pointer to unused cell, where we can store what we need stack[SP] = a * 3; return stack[SP]; }

uso ( fragmento # 4.3 ):

// SP == 1000 someVar = triple_noLocals(11); // now SP == 999, but we don''t need the value at stack[999] anymore // and we will move the stack index back, so we can reuse this cell later SP += 1; // SP == 1000 again

5. Push / pop

La adición de un nuevo elemento en la parte superior de la pila es una operación tan frecuente, que las CPU tienen una instrucción especial para eso, push . Lo implementaremos así ( snippet 5.1 ):

void push(int value) { --SP; stack[SP] = value; }

Del mismo modo, tomando el elemento superior de la pila ( fragmento 5.2 ):

void pop(int& result) { result = stack[SP]; ++SP; // note that `pop` decreases stack''s size }

El patrón de uso común para push / pop está guardando temporalmente algún valor. Digamos que tenemos algo útil en la variable myVar y por alguna razón necesitamos hacer cálculos que lo sobreescribirán ( fragmento 5.3 ):

int myVar = ...; push(myVar); // SP == 999 myVar += 10; ... // do something with new value in myVar pop(myVar); // restore original value, SP == 1000

6. Deshacerse de los parámetros

Ahora pasemos los parámetros usando la pila ( fragmento # 6 ):

int triple_noL_noParams() { // `a` is at index 999, SP == 999 SP -= 1; // SP == 998, stack[SP + 1] == a stack[SP] = stack[SP + 1] * 3; return stack[SP]; } int main(){ push(11); // SP == 999 assert(triple(11) == triple_noL_noParams()); SP += 2; // cleanup 1 local and 1 parameter }

7. Deshacerse de declaraciones de return

Devuelvamos el valor en el registro AX ( fragmento # 7 ):

void triple_noL_noP_noReturn() { // `a` at 998, SP == 998 SP -= 1; // SP == 997 stack[SP] = stack[SP + 1] * 3; AX = stack[SP]; SP += 1; // finally we can cleanup locals right in the function body, SP == 998 } void main(){ ... // some code push(AX); // save AX in case there is something useful there, SP == 999 push(11); // SP == 998 triple_noL_noP_noReturn(); assert(triple(11) == AX); SP += 1; // cleanup param // locals were cleaned up in the function body, so we don''t need to do it here pop(AX); // restore AX ... }

8. Puntero base de pila (BP) (también conocido como puntero de marco ) y marco de pila

Permite tomar más funciones "avanzadas" y reescribirlas en nuestro C ++ tipo asm ( fragmento # 8.1 ):

int myAlgo(int a, int b) { int t1 = a * 3; int t2 = b * 3; return t1 - t2; } void myAlgo_noLPR() { // `a` at 997, `b` at 998, old AX at 999, SP == 997 SP -= 2; // SP == 995 stack[SP + 1] = stack[SP + 2] * 3; stack[SP] = stack[SP + 3] * 3; AX = stack[SP + 1] - stack[SP]; SP += 2; // cleanup locals, SP == 997 } int main(){ push(AX); // SP == 999 push(22); // SP == 998 push(11); // SP == 997 myAlgo_noLPR(); assert(myAlgo(11, 22) == AX); SP += 2; pop(AX); }

Ahora imagina que decidimos introducir una nueva variable local para almacenar el resultado allí antes de regresar, como hacemos en tripple (fragmento # 4.1). El cuerpo de la función será ( fragmento # 8.2 ):

SP -= 3; // SP == 994 stack[SP + 2] = stack[SP + 3] * 3; stack[SP + 1] = stack[SP + 4] * 3; stack[SP] = stack[SP + 2] - stack[SP + 1]; AX = stack[SP]; SP += 3;

Verán, tuvimos que actualizar cada referencia a los parámetros de función y las variables locales. Para evitar eso, necesitamos un índice de anclaje, que no cambie cuando la pila crezca.

Crearemos el ancla justo después de la entrada de la función (antes de asignar espacio para los locales) guardando la parte superior actual (valor de SP) en el registro BP. Snippet # 8.3 :

void myAlgo_noLPR_withAnchor() { // `a` at 997, `b` at 998, SP == 997 push(BP); // save old BP, SP == 996 BP = SP; // create anchor, stack[BP] == old value of BP, now BP == 996 SP -= 2; // SP == 994 stack[BP - 1] = stack[BP + 1] * 3; stack[BP - 2] = stack[BP + 2] * 3; AX = stack[BP - 1] - stack[BP - 2]; SP = BP; // cleanup locals, SP == 996 pop(BP); // SP == 997 }

El trozo de pila, que pertenece y tiene el control total de la función, se llama marco de pila de la función . Por myAlgo_noLPR_withAnchor , el marco de pila myAlgo_noLPR_withAnchor es stack[996 .. 994] (ambos idexes inclusive).
El fotograma se inicia en el BP de la función (después de que lo hayamos actualizado dentro de la función) y dura hasta el siguiente fotograma de la pila. Entonces, los parámetros en la pila son parte del marco de pila de la persona que llama (ver nota 8a).

Notas:
8a. Wikipedia dice lo contrario sobre los parámetros, pero aquí me adhiero al manual del desarrollador de software Intel , ver vol. 1, sección 6.2.4.1 Puntero de base del cuadro de apilamiento y Figura 6-2 en la sección 6.3.2 Operación de llamada a distancia y RET . Los parámetros de la función y el marco de la pila son parte del registro de activación de la función (ver El gen en los perilogios de función ).
8b. los desplazamientos positivos desde el punto BP a los parámetros de función y los desplazamientos negativos apuntan a las variables locales. Eso es bastante útil para la depuración
8c. stack[BP] almacena la dirección del marco de pila anterior, la stack[stack[BP]] almacena el marco de pila previo, y así sucesivamente. Siguiendo esta cadena, puede descubrir marcos de todas las funciones en el programa, que aún no han regresado. Así es como los depuradores te muestran la pila de llamadas
8d. las primeras 3 instrucciones de myAlgo_noLPR_withAnchor , donde configuramos el marco (guardar el antiguo BP, actualizar BP, reservar espacio para los locales) se denominan prólogo de funciones

9. Convenciones de llamada

En el fragmento 8.1, hemos introducido parámetros para myAlgo de derecha a izquierda y hemos devuelto el resultado en AX . También podríamos pasar los parámetros de izquierda a derecha y regresar en BX . O pase los parámetros en BX y CX y regrese en AX. Obviamente, la función llamante ( main() ) y llamada debe acordar dónde y en qué orden se almacena todo esto.

La convención de llamadas es un conjunto de reglas sobre cómo se pasan los parámetros y se devuelve el resultado.

En el código anterior hemos utilizado la convención de llamadas cdecl :

  • Los parámetros se pasan en la pila, con el primer argumento en la dirección más baja en la pila en el momento de la llamada (presionado last <...>). La persona que llama es responsable de recuperar los parámetros de la pila después de la llamada.
  • el valor de retorno se coloca en AX
  • EBP y ESP deben ser preservados por la función callee ( myAlgo_noLPR_withAnchor en nuestro caso), de modo que la persona que llama (función main ) pueda confiar en esos registros que no han sido modificados por una llamada.
  • Todos los demás registros (EAX, <...>) pueden ser modificados libremente por el destinatario; si una persona que llama desea conservar un valor antes y después de la llamada a la función, debe guardar el valor en otro lugar (lo hacemos con AX)

(Fuente: ejemplo "32-bit cdecl" de Documentation, copyright 2016 de icktoofay y Peter Cordes ; licencia bajo CC BY-SA 3.0. Un archivo del contenido completo de Documentation se puede encontrar en archive.org, en el cual este ejemplo está indexado por tema ID 3261 y ejemplo ID 11196.)

10. Deshacerse de las llamadas a funciones

Ahora la parte más interesante. Al igual que los datos, el código ejecutable también se almacena en la memoria (sin relación con la memoria para la pila) y cada instrucción tiene una dirección.
Cuando no se ordena de otra manera, la CPU ejecuta las instrucciones una tras otra, en el orden en que están almacenadas en la memoria. Pero podemos ordenarle a la CPU que "salte" a otra ubicación en la memoria y ejecute las instrucciones desde allí. En ASM puede ser cualquier dirección, y en más lenguajes de alto nivel como C ++, solo puede saltar a direcciones marcadas por etiquetas ( hay soluciones pero no son bonitas, por decir lo menos).

Tomemos esta función ( fragmento # 10.1 ):

int myAlgo_withCalls(int a, int b) { int t1 = triple(a); int t2 = triple(b); return t1 - t2; }

Y en lugar de llamar al modo tripple C ++, haga lo siguiente:

  1. Copiar el cuerpo de tripple enteros dentro de myAlgo
  2. en myAlgo entry salta sobre el código de goto con goto
  3. cuando necesitamos ejecutar el código de tripple , guardamos en la dirección de pila de la línea de código justo después de la llamada tripple , para que podamos regresar más tarde y continuar la ejecución (macro PUSH_ADDRESS continuación)
  4. salta a la dirección de la función tripple y ejecútalo hasta el final (3. y 4. juntos son macro CALL )
  5. al final del tripple (después de que hayamos limpiado a los locales), toma la dirección de retorno desde la parte superior de la pila y salta allí (macro RET )

Debido a que no hay una manera fácil de saltar a una dirección de código particular en C ++, utilizaremos etiquetas para marcar lugares de saltos. No entraré en detalles sobre cómo funcionan las macros a continuación, solo créanme que hacen lo que digo que hacen ( fragmento # 10.2 ):

// pushes the address of the code at label''s location on the stack // NOTE1: this gonna work only with 32-bit compiler (so that pointer is 32-bit and fits in int) // NOTE2: __asm block is specific for Visual C++. In GCC use https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html #define PUSH_ADDRESS(labelName) { / void* tmpPointer; / __asm{ mov [tmpPointer], offset labelName } / push(reinterpret_cast<int>(tmpPointer)); / } // why we need indirection, read https://.com/a/13301627/264047 #define TOKENPASTE(x, y) x ## y #define TOKENPASTE2(x, y) TOKENPASTE(x, y) // generates token (not a string) we will use as label name. // Example: LABEL_NAME(155) will generate token `lbl_155` #define LABEL_NAME(num) TOKENPASTE2(lbl_, num) #define CALL_IMPL(funcLabelName, callId) / PUSH_ADDRESS(LABEL_NAME(callId)); / goto funcLabelName; / LABEL_NAME(callId) : // saves return address on the stack and jumps to label `funcLabelName` #define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__) // takes address at the top of stack and jump there #define RET() { / int tmpInt; / pop(tmpInt); / void* tmpPointer = reinterpret_cast<void*>(tmpInt); / __asm{ jmp tmpPointer } / } void myAlgo_asm() { goto my_algo_start; triple_label: push(BP); BP = SP; SP -= 1; // stack[BP] == old BP, stack[BP + 1] == return address stack[BP - 1] = stack[BP + 2] * 3; AX = stack[BP - 1]; SP = BP; pop(BP); RET(); my_algo_start: push(BP); // SP == 995 BP = SP; // BP == 995; stack[BP] == old BP, // stack[BP + 1] == dummy return address, // `a` at [BP + 2], `b` at [BP + 3] SP -= 2; // SP == 993 push(AX); push(stack[BP + 2]); CALL(triple_label); stack[BP - 1] = AX; SP -= 1; pop(AX); push(AX); push(stack[BP + 3]); CALL(triple_label); stack[BP - 2] = AX; SP -= 1; pop(AX); AX = stack[BP - 1] - stack[BP - 2]; SP = BP; // cleanup locals, SP == 997 pop(BP); } int main() { push(AX); push(22); push(11); push(7777); // dummy value, so that offsets inside function are like we''ve pushed return address myAlgo_asm(); assert(myAlgo_withCalls(11, 22) == AX); SP += 1; // pop dummy "return address" SP += 2; pop(AX); }

Notas:
10 a. porque la dirección de retorno se almacena en la pila, en principio podemos cambiarla. Así es como funciona el ataque smashing
10b. las últimas 3 instrucciones en el "final" de triple_label (limpieza local, restauración de BP anterior, devolución) se llaman epílogo de función

11. Asamblea

Ahora veamos asm real para myAlgo_withCalls . Para hacer eso en Visual Studio:

  • establecer la plataforma de compilación para x86
  • tipo de compilación: Depurar
  • establecer punto de interrupción en algún lugar dentro de myAlgo_withCalls
  • ejecutar, y cuando la ejecución se detiene en el punto de ruptura, presione Ctrl + Alt + D

Una diferencia con nuestro C ++ de tipo asm es que la pila de Asm opera en bytes en lugar de en Ints. Entonces, para reservar espacio para un int , el SP se reducirá en 4 bytes.
Aquí vamos ( fragmento # 11.1 , los números de línea en los comentarios son de la gist ):

; 114: int myAlgo_withCalls(int a, int b) { push ebp ; create stack frame mov ebp,esp ; return address at (ebp + 4), `a` at (ebp + 8), `b` at (ebp + 12) sub esp,0D8h ; reserve space for locals. Compiler can reserve more bytes then needed. 0D8h is hexadecimal == 216 decimal push ebx ; cdecl requires to save all these registers push esi push edi ; fill all the space for local variables (from (ebp-0D8h) to (ebp)) with value 0CCCCCCCCh repeated 36h times (36h * 4 == 0D8h) ; see https://.com/q/3818856/264047 ; I guess that''s for ease of debugging, so that stack is filled with recognizable values ; 0CCCCCCCCh in binary is 110011001100... lea edi,[ebp-0D8h] mov ecx,36h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] ; 115: int t1 = triple(a); mov eax,dword ptr [ebp+8] ; push parameter `a` on the stack push eax call triple (01A13E8h) add esp,4 ; clean up param mov dword ptr [ebp-8],eax ; copy result from eax to `t1` ; 116: int t2 = triple(b); mov eax,dword ptr [ebp+0Ch] ; push `b` (0Ch == 12) push eax call triple (01A13E8h) add esp,4 mov dword ptr [ebp-14h],eax ; t2 = eax mov eax,dword ptr [ebp-8] ; calculate and store result in eax sub eax,dword ptr [ebp-14h] pop edi ; restore registers pop esi pop ebx add esp,0D8h ; check we didn''t mess up esp or ebp. this is only for debug builds cmp ebp,esp call __RTC_CheckEsp (01A116Dh) mov esp,ebp ; destroy frame pop ebp ret

Y asm para tripple ( fragmento # 11.2 ):

push ebp mov ebp,esp sub esp,0CCh push ebx push esi push edi lea edi,[ebp-0CCh] mov ecx,33h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] imul eax,dword ptr [ebp+8],3 mov dword ptr [ebp-8],eax mov eax,dword ptr [ebp-8] pop edi pop esi pop ebx mov esp,ebp pop ebp ret

Espero, después de leer esta publicación, el montaje no parece tan críptico como antes :)

Aquí hay enlaces del cuerpo de la publicación y algunas lecturas adicionales:


Confundes una pila abstracta y la pila implementada por hardware. Este último ya está implementado.


Creo que la respuesta principal que está buscando ya ha sido insinuada.

Cuando se inicia una computadora x86, la pila no está configurada. El programador debe configurarlo explícitamente en el momento del arranque. Sin embargo, si ya está en un sistema operativo, esto se ha solucionado. A continuación se muestra un ejemplo de código de un programa de arranque simple.

Primero se establecen los registros de datos y segmentos de pila, y luego el puntero de pila se establece en 0x4000 más allá de eso.

movw $BOOT_SEGMENT, %ax movw %ax, %ds movw %ax, %ss movw $0x4000, %ax movw %ax, %sp

Después de este código, se puede usar la pila. Ahora estoy seguro de que se puede hacer de diferentes maneras, pero creo que esto debería ilustrar la idea.


Creo que principalmente te confundes entre program''s stack un program''s stack y any old stack .

Un montón

Es una estructura de datos abstractos que consiste en información en un sistema Last In First Out. Colocas objetos arbitrarios en la pila y luego los quitas de nuevo, al igual que una bandeja de entrada / salida, el elemento superior siempre es el que se quita y siempre se coloca en la parte superior.

Una pila de programas

Es una pila, es una sección de memoria que se usa durante la ejecución, generalmente tiene un tamaño estático por programa y se usa frecuentemente para almacenar parámetros de funciones. Usted inserta los parámetros en la pila cuando llama a una función y la función dirige la pila directamente o muestra las variables de la pila.

Una pila de programas generalmente no es hardware (aunque se mantiene en la memoria por lo que se puede argumentar como tal), pero el puntero de pila que apunta a un área actual de la pila generalmente es un registro de la CPU. Esto lo hace un poco más flexible que una pila LIFO ya que puede cambiar el punto al que se dirige la pila.

Debes leer y asegurarte de que entiendes el artículo de la wikipedia ya que da una buena descripción de la Pila de Hardware, que es lo que estás tratando.

También hay este tutorial que explica la pila en términos de los viejos registros de 16 bits, pero podría ser útil y otro específicamente sobre la pila.

De Nils Pipenbrinck:

Vale la pena señalar que algunos procesadores no implementan todas las instrucciones para acceder y manipular la pila (push, pop, stack puntero, etc.), pero el x86 sí lo hace debido a su frecuencia de uso. En estas situaciones, si quería una pila, tendría que implementarla usted mismo (algunos MIPS y algunos procesadores ARM se crean sin pilas).

Por ejemplo, en los MIP una instrucción push se implementaría como:

addi $sp, $sp, -4 # Decrement stack pointer by 4 sw $t0, ($sp) # Save $t0 to stack

y una instrucción Pop se vería así:

lw $t0, ($sp) # Copy from stack to $t0 addi $sp, $sp, 4 # Increment stack pointer by 4


En cuanto a si la pila se implementa en el hardware, este artículo de Wikipedia podría ayudar.

Algunas familias de procesadores, como la x86, tienen instrucciones especiales para manipular la pila del hilo que se está ejecutando actualmente. Otras familias de procesadores, incluidos PowerPC y MIPS, no tienen soporte de pila explícito, sino que confían en la convención y delegan la administración de la pila en la Interfaz Binaria de Aplicación (ABI) del sistema operativo.

Ese artículo y los otros con los que se enlaza podrían ser útiles para tener una idea del uso de la pila en los procesadores.


La pila es solo una forma en que los programas y las funciones usan la memoria.

La pila siempre me confundió, así que hice una ilustración:

( versión svg aquí )


La pila se "implementa" por medio del puntero de pila, que (suponiendo que la arquitectura x86 aquí) apunta al segmento de pila. Cada vez que se empuja algo en la pila (mediante pushl, call o un código de pila similar), se escribe en la dirección apuntada por el puntero de pila, y el puntero de pila disminuye (la pila crece hacia abajo , es decir, direcciones más pequeñas) . Cuando saca algo de la pila (pop, ret), el puntero de la pila se incrementa y el valor se lee en la pila.

En una aplicación de espacio de usuario, la pila ya está configurada cuando se inicia la aplicación. En un entorno de espacio de kernel, primero debe configurar el segmento de pila y el puntero de pila ...


La pila ya existe, por lo que puedes asumir eso al escribir tu código. La pila contiene las direcciones de retorno de las funciones, las variables locales y las variables que se pasan entre las funciones. También hay registros de pila como BP, SP (Stack Pointer) integrados que puede usar, de ahí los comandos incorporados que ha mencionado. Si la pila aún no se había implementado, las funciones no podrían ejecutarse y el flujo de código podría no funcionar.


No he visto el ensamblador de Gas específicamente, pero en general la pila se "implementa" al mantener una referencia a la ubicación en la memoria donde reside la parte superior de la pila. La ubicación de la memoria se almacena en un registro, que tiene diferentes nombres para diferentes arquitecturas, pero se puede considerar como el registro del puntero de la pila.

Los comandos pop y push se implementan en la mayoría de las arquitecturas basándose en microinstrucciones. Sin embargo, algunas "arquitecturas educativas" requieren que las implementes tú mismo. Funcionalmente, push se implementaría algo así:

load the address in the stack pointer register to a gen. purpose register x store data y at the location x increment stack pointer register by size of y

Además, algunas arquitecturas almacenan la última dirección de memoria utilizada como Stack Pointer. Algunos almacenan la siguiente dirección disponible.


Tiene razón en que una pila es una estructura de datos. A menudo, las estructuras de datos (pilas incluidas) con las que trabajas son abstractas y existen como una representación en la memoria.

La pila con la que está trabajando en este caso tiene una existencia más material: se asigna directamente a registros físicos reales en el procesador. Como estructura de datos, las pilas son estructuras FILO (primero en entrar, último en salir) que aseguran que los datos se eliminen en el orden inverso en que se ingresaron. ¡Vea el logo de para una presentación visual! ;)

Estás trabajando con la pila de instrucciones . This is the stack of actual instructions you are feeding the processor.


stack is part of memory. it use for input and output of functions . also it use for remembering function''s return.

esp register is remember the stack address.

stack and esp are implemented by hardware. also you can implement it yourself. it will make your program very slow.

ejemplo:

nop // esp = 0012ffc4

push 0 // esp = 0012ffc0 ,Dword[0012ffc0]=00000000

call proc01 // esp = 0012ffbc ,Dword[0012ffbc] = eip , eip = adrr[proc01]

pop eax // eax = Dword[ esp ], esp = esp + 4


El concepto

Primero piensa en todo como si fueras la persona que lo inventó. Me gusta esto:

Primero piense en una matriz y cómo se implementa en el nivel bajo -> es básicamente un conjunto de ubicaciones de memoria contigua (ubicaciones de memoria que están una al lado de la otra). Ahora que tiene esa imagen mental en la cabeza, piense en el hecho de que puede acceder a CUALQUIERA de esas ubicaciones de memoria y eliminarla cuando lo desee a medida que elimina o agrega datos en su matriz. Ahora piense en la misma matriz, pero en lugar de la posibilidad de eliminar cualquier ubicación, usted decide que eliminará solo la ÚLTIMA ubicación a medida que elimina o agrega datos en la matriz. Ahora su nueva idea para manipular los datos en esa matriz de esa manera se llama LIFO, que significa Last In First Out. Su idea es muy buena porque facilita el seguimiento del contenido de esa matriz sin tener que usar un algoritmo de clasificación cada vez que elimina algo de ella. Además, para saber en todo momento cuál es la dirección del último objeto de la matriz, dedique un Registro en la CPU para realizar un seguimiento de la misma. Ahora, la forma en que el registro realiza un seguimiento de esto es para que cada vez que elimine o agregue algo a su matriz también disminuya o incremente el valor de la dirección en su registro por la cantidad de objetos que eliminó o agregó de la matriz (por la cantidad de espacio de direcciones que ocuparon). También debe asegurarse de que la cantidad por la que disminuya o incremente ese registro se fije en una cantidad (como 4 ubicaciones de memoria, es decir, 4 bytes) por objeto, de nuevo, para que sea más fácil hacer un seguimiento y también para que sea posible utilizar ese registro con algunas construcciones de bucle porque los bucles usan incrementos fijos por iteración (por ejemplo, para recorrer su matriz con un bucle, construya el bucle para incrementar su registro en 4 cada iteración, lo que no sería posible si su matriz tiene objetos de diferentes tamaños en ella). Por último, elige llamar a esta nueva estructura de datos una "Pila", porque le recuerda a una pila de platos en un restaurante donde siempre quitan o agregan un plato en la parte superior de esa pila.

La implementación

Como puede ver, una pila no es más que una matriz de ubicaciones de memoria contigua donde decidió cómo manipularla. Por eso puede ver que no necesita ni siquiera usar las instrucciones especiales y los registros para controlar la pila. Puede implementarlo usted mismo con las instrucciones básicas mov, add y subins y usando registros de propósito general en lugar de ESP y EBP como este:

mov edx, 0FFFFFFFFh

; -> esta será la dirección de inicio de tu pila, más alejada de tu código y datos, también servirá como ese registro que hace un seguimiento del último objeto en la pila que expliqué antes. Usted lo llama el "puntero de la pila", por lo que elige el registro EDX para qué se utiliza ESP normalmente.

sub edx, 4

mov [edx], dword ptr [someVar]

; -> estas dos instrucciones disminuirán su puntero de pila en 4 ubicaciones de memoria y copiará los 4 bytes comenzando en [algunaVar] ubicación de memoria en la ubicación de memoria a la que ahora apunta EDX, al igual que una instrucción PUSH disminuye el ESP, solo aquí lo hizo es manual y usaste EDX. Entonces la instrucción PUSH es básicamente solo un código de operación más corto que realmente hace esto con ESP.

mov eax, dword ptr [edx]

agregar edx, 4

; -> y aquí hacemos lo contrario, primero copiamos los 4 bytes comenzando en la ubicación de la memoria que EDX ahora señala en el registro EAX (elegido arbitrariamente aquí, podríamos haberlo copiado en cualquier lugar que quisiéramos). Y luego incrementamos nuestro puntero de pila EDX en 4 ubicaciones de memoria. Esto es lo que hace la instrucción POP.

Ahora puede ver que las instrucciones PUSH y POP y los registros ESP ans EBP fueron añadidos por Intel para hacer que el concepto anterior de la estructura de datos de "pila" sea más fácil de escribir y leer. Todavía hay algunos Cpu-s RISC (Conjunto de instrucciones reducido) que no tienen las instrucciones PUSH ans POP y registros dedicados para la manipulación de pila, y al escribir programas de ensamblaje para esas Cpu-s usted tiene que implementar la pila por sí mismo al igual que te lo mostré


Calling functions, which requires saving and restoring local state in LIFO fashion (as opposed to say, a generalized co-routine approach), turns out to be such an incredibly common need that assembly languages and CPU architectures basically build this functionality in. The same could probably be said for notions of threading, memory protection, security levels, etc. In theory you could implement your own stack, calling conventions, etc., but I assume some opcodes and most existing runtimes rely on this native concept of "stack".


I was searching about how stack works in terms of function and i found this blog its awesome and its explain concept of stack from scratch and how stack store value in stack.

Now on your answer . I will explain with python but you will get good idea how stack works in any language.

Its a program :

def hello(x): if x==1: return "op" else: u=1 e=12 s=hello(x-1) e+=1 print(s) print(x) u+=1 return e hello(3)

Fuente: Cryptroix

algunos de los temas que cubre en el blog:

How Function work ? Calling a Function Functions In a Stack What is Return Address Stack Stack Frame Call Stack Frame Pointer (FP) or Base Pointer (BP) Stack Pointer (SP) Allocation stack and deallocation of stack What is Heap?

Pero se explica con el lenguaje python, así que si quieres puedes echarle un vistazo.


The call stack is implemented by the x86 instruction set and the operating system.

Instructions like push and pop adjust the stack pointer while the operating system takes care of allocating memory as the stack grows for each thread.

The fact that the x86 stack "grows down" from higher to lower addresses make this architecture more susceptible to the buffer overflow attack.


You are correct that a stack is ''just'' a data structure. Here, however, it refers to a hardware implemented stack used for a special purpose --"The Stack".

Many people have commented about hardware implemented stack versus the (software)stack data structure. I would like to add that there are three major stack structure types -

  1. A call stack -- Which is the one you are asking about! It stores function parameters and return address etc. Do read Chapter 4 ( All about 4th page ie page 53)functions in that book. There is a good explanation.
  2. A generic stack Which you might use in your program to do something special...
  3. A generic hardware stack
    I am not sure about this, but I remember reading somewhere that there is a general purpose hardware implemented stack available in some architectures. If anyone knows whether this is correct, please do comment.

The first thing to know is the architecture you are programming for, which the book explains (I just looked it up --link). To really understand things, I suggest that you learn about the memory, addressing, registers and architecture of x86 (I assume thats what you are learning --from the book).