2009-09-03 13 views
6

Tengo una consulta con respecto a los punteros, ¿alguien me puede ayudar a explicarme lo siguiente?Ayuda con C Pointers

Entiendo cómo funcionan los punteros, sin embargo, no estoy muy seguro de cómo sobrescribir las partes de la memoria de las direcciones para modificar el comportamiento del programa.

voy a explicar lo siguiente como todo lo que pueda de acuerdo a lo que entiendo, no dude en crítica y me ilumine en mis malos entendidos, aquí está el código trozo:

void f(int) ; 
int main (int argc, char ** argv) { 
    int a = 1234 ; 
    f(a); 
    printf("Back to main\n") ; 
} 
void g() { 
    printf("Inside g\n") ; 
} 
void f (int x) { 
    int a[100] ; 
    memcpy((char*)a,(char*)g,399) ; 
    x = *(&x-1) ; 
    *(&x-1) = (int)(&a) ; // note the cast; no cast -> error 
    // find an index for a such that a[your_index] is the same as x 
    printf("About to return from f\n") ; 
} 

//This program, compiled with the same compiler as above, produces the following output: 

//About to return from f 
//Inside g 
//Back to main 

Ok por lo que entiendo, esto es como va.

El programa comienza procedimentally frorm main(), asigna un, luego entra en f() con una variable como.

Dentro de f():

Se INITs una matriz A de tamaño 100. Entonces copias el espacio de memoria de g() a todo el un array. Entonces, esencialmente, un [] es g(). x se asigna a la dirección del original a desde main() - 1, que asumiría que es la dirección de main(). (No estoy seguro de esto, corríjame si estoy equivocado)

De aquí en adelante, no estoy muy seguro de cómo se puede llamar a [] (el que se sobreescribe con g()) o incluso g(). Simplemente parece terminar f() y volver a main().

¡Gracias a quien me puede ayudar con esto!

¡Salud!

+0

tengo actualicé sustancialmente mi respuesta para poder responder sus preguntas adicionales en el comentario. – caf

Respuesta

20

Técnicamente, ese código va más allá de lo que define el estándar C, por lo que podría hacer cualquier cosa. Está haciendo una gran cantidad de suposiciones a las que no tiene derecho, y esas suposiciones ciertamente no son universalmente ciertas. Sin embargo, puedo presentar una explicación muy probable de por qué ve la salida que hace:

Está en lo correcto hasta el punto donde ha copiado el código de la función g() en la memoria ocupada por la variable de matriz local a.

Para comprender la próxima línea, necesita saber un poco acerca de cómo se suelen llamar las funciones en las arquitecturas comunes basadas en la pila. Cuando se invoca una función, los parámetros se envían a la pila, luego se inserta la dirección de retorno en la pila, y la ejecución salta al punto de inicio de la función. Dentro de la función, se inserta el puntero de marco anterior en la pila, luego se hace espacio para las variables locales. Las pilas tienden a crecer hacia abajo en la memoria (desde direcciones altas a direcciones bajas), aunque este no es el caso en todas las arquitecturas comunes.

lo tanto, cuando principales llamadas en función de f(), la pila se ve inicialmente como esto (el puntero marco y puntero de pila son dos registros de la CPU que contengan direcciones de lugares en la pila):

    | ...      | (higher addresses) 
        | char **argv (parameter) | 
        |-------------------------| 
        | int argc (parameter) | 
        |-------------------------| 
FRAME POINTER -> | saved frame pointer  | 
        |-------------------------| 
        | int a     | 
        |-------------------------| 
        | int x (parameter)  | &x 
        |-------------------------| 
STACK POINTER -> | return address   | &x - 1 
        |-------------------------| 
        | ...      | (lower addresses) 

El prólogo de función guarda el puntero de marco de la función de llamada y mueve el puntero de pila para crear espacio para las variables locales en f(). Así que cuando el código C en f() comienza a ejecutar, la pila ahora se ve algo como esto:

    | ...      | (higher addresses) 
        | char **argv (parameter) | 
        |-------------------------| 
        | int argc (parameter) | 
        |-------------------------| 
        | saved frame pointer  | 
        |-------------------------| 
        | int a     | 
        |-------------------------| 
        | int x (parameter)  | &x 
        |-------------------------| 
        | return address   | &x - 1 
        |-------------------------| 
FRAME POINTER -> | saved frame pointer  | 
        |-------------------------| 
        | a[99]     | &a[99] 
        | a[98]     | &a[98] 
        | ...      | ... 
STACK POINTER -> | a[0]     | &a[0] 
        | ...      | (lower addresses) 

¿Cuál es el puntero de marco? Se usa para hacer referencia a variables y parámetros locales dentro de una función. El compilador sabe que cuando se está ejecutando f(), la dirección de la variable local a es siempreFRAME_POINTER - 100 * sizeof(int), y la dirección del parámetro x es FRAME_POINTER + sizeof(FRAME_POINTER) + sizeof(RETURN_ADDRESS). Se puede acceder a todas las variables y parámetros locales como un desplazamiento fijo desde el puntero del marco, sin importar cómo se mueve el puntero de la pila a medida que se asigna y se desasigna el espacio de la pila.

De todos modos, volvamos al código. Cuando esta línea realiza:

x = *(&x-1) ; 

Se copia el valor que se almacena 1 número entero de tamaño menor en la memoria que x, en x. Si miras mi ASCII-art, verás que esa es la dirección de retorno.Así que en realidad llevar a cabo esto:

x = RETURN_ADDRESS; 

La siguiente línea:

*(&x-1) = (int)(&a) ; 

a continuación, establece la dirección de retorno a la dirección de la matriz a. Que en realidad está diciendo:

RETURN_ADDRESS = &a; 

Se requiere que el yeso porque se está tratando la dirección de retorno como un int, y no un puntero (por lo que, de hecho, este código única trabajo en arquitecturas donde int es el mismo tamaño como un puntero - ¡esto NO funcionará en sistemas POSIX de 64 bit, por ejemplo!).

Ahora se completa el código C en la función f(), y el epílogo de función desasigna las variables locales (moviendo el puntero de pila hacia atrás) y restaura el puntero de marco de la persona que llama. En este punto, la pila se ve como:

    | ...      | (higher addresses) 
        | char **argv (parameter) | 
        |-------------------------| 
        | int argc (parameter) | 
        |-------------------------| 
FRAME POINTER -> | saved frame pointer  | 
        |-------------------------| 
        | int a     | 
        |-------------------------| 
        | int x (parameter)  | &x 
        |-------------------------| 
STACK POINTER -> | return address   | &x - 1 
        |-------------------------| 
        | saved frame pointer  | 
        |-------------------------| 
        | a[99]     | &a[99] 
        | a[98]     | &a[98] 
        | ...      | ... 
        | a[0]     | &a[0] 
        | ...      | (lower addresses) 

ahora la función devuelve al saltar al valor de RETURN_ADDRESS - pero establece que a &a, así que en vez de ir de nuevo a donde fue llamado desde, salta a el valor de inicio de la matriz a - ahora está ejecutando el código de la pila. Aquí es donde copió el código de la función g(), por lo que el código (aparentemente) se ejecuta felizmente. Tenga en cuenta que debido a que el puntero de la pila se ha movido hacia atrás sobre la matriz aquí, cualquier código asíncrono que se ejecute con la misma pila (como una señal UNIX que llega en el momento incorrecto) sobrescribirá el código.

Así que aquí es lo que la pila ahora se ve como en el inicio de g(), antes de que el prólogo de la función:

    | ...      | (higher addresses) 
        | char **argv (parameter) | 
        |-------------------------| 
        | int argc (parameter) | 
        |-------------------------| 
FRAME POINTER -> | saved frame pointer  | 
        |-------------------------| 
        | int a     | 
        |-------------------------| 
STACK POINTER -> | int x (parameter)  | 
        |-------------------------| 
        | return address   | 
        |-------------------------| 
        | saved frame pointer  | 
        |-------------------------| 
        | a[99]     | 
        | a[98]     | 
        | ...      | 
        | a[0]     | 
        | ...      | (lower addresses) 

El prólogo de g() continuación, establece una estructura de pila de forma normal, lo ejecuta, y desenrolla, que deja el puntero del marco y el puntero de la pila como en el último diagrama de arriba.

ahora g() devoluciones, por lo que busca un valor de retorno en la parte superior de la pila - pero la parte superior de la pila (en la que el puntero de pila está apuntando) es en realidad el lugar donde el parámetro x funcione f() vivieron - y aquí es donde guardamos el valor de retorno original anterior, por lo que vuelve al lugar desde donde se llamó a f().

Como nota al margen, la pila está ahora desincronizados en main(), ya que espera que el puntero de pila para estar donde estaba cuando se llama f() (que está apuntando hacia el lugar donde se almacena el parámetro x) - pero ahora es en realidad apuntando a la variable local a. Esto causaría algunos efectos extraños: si llamara a otra función desde main en este punto, el contenido de a se alteraría.

espero que usted (y otros) han aprendido algo valioso de esta discusión, pero es importante recordar que esto es como la Técnica de cinco puntos palma de estallido del corazón de programación - NUNCA lo utilizan en un sistema real. Una nueva sub-arquitectura, compilador o incluso solo diferentes banderas de compilación pueden cambiar el entorno de ejecución lo suficiente como para hacer que este tipo de código demasiado inteligente por medio falle por completo en todo tipo de formas deliciosas y divertidas.

+2

+1 para su arte ASCII (ch). Y por una maldita buena explicación. –

+0

increíblemente detalles y una explicación increíble. ¡GRACIAS! – nubela

+0

detallado * (15chars) – nubela

2

Llamar a una función con argumentos "por valor" no hace que los argumentos sean modificables por la función. Los enteros simples se pasan por valor, cuando llama al f(a); desde main(), que no hace que la función f pueda cambiar el valor de a, solo obtiene el valor. Si desea cambiar la variable original, debe llamar por referencia, es decir, f(&a);, después de cambiar la función para aceptar un puntero, por supuesto.

Es un poco ... inútil discutir sobre qué esperar cuando haces cosas indefinidas, como sobrescribir la memoria. También intentar copiar el código de una función desde su dirección no es muy seguro.

3

OK, esta es solo una posible explicación de lo que está sucediendo desde que, como se mencionó, al sobrescribir direcciones de memoria "importantes" puede pasar cualquier cosa.

Dicho esto, parece que lo que está sucediendo es algo como esto:

  • Se llama a f() de la principal. La dirección de retorno (la llamada a printf) se inserta en la pila seguida del valor de a.
  • Copie el código de g() a a [].
  • (Cambia x, pero eso no hace nada).
  • Sobreescribes la dirección de retorno en la pila con la dirección de un [] (que contiene una copia del código de g()).
  • f() regresa al código en un código [], ejecutando g().

De nuevo, esto es todo especulación: depende del compilador, las opciones del compilador y la plataforma en la que esté ejecutando esto.

+0

Sí, tiene razón, la dirección de retorno está siendo modificada para apuntar a la copia de g(). Esto es muy específico de la plataforma y no es portátil en absoluto. La parte realmente extraña es cómo vuelve a lo principal. – Skizz

+0

Skizz: Vea mi respuesta. El parámetro 'x' se convierte en la dirección de retorno de' g() '(bueno, la * copia * de' g() ', de todos modos!) – caf

0

En a, tiene toda la función 'g'. Con cambiar 'x' los puntos x en el punto de retorno original otro cambio de X, y sobrescribirlo con el puntero a 'a' cambiará el valor de retorno de la función principal, pero de hecho, no estoy seguro de la salida, dependerá de las optimizaciones utilizadas.

3

Bien, he averiguado cómo se vuelve a lo principal (ver comentario a la respuesta de Tal).

Necesita saber cómo funciona la pila, específicamente, en una CPU Intel.

En principal, la pila se ve así:

stacktop: 1234 - the a variable, locals are normally on the stack) 

al comienzo de f (x) Parece que este

stacktop: 1234 - main's a 
-1   1234 - the argument to f(), pushed onto the stack 
-2   ret_addr - points to the printf in main, where f() will go when it's finished 
-3   a[99] 
-4   a[98] 
      ... 
-101  a[1] 
-102  a[0] 

Pilas crecen de arriba a abajo.

El código "(& x-1)" apunta a stacktop-2 en este caso desde & x es la dirección del parámetro pasado a f() que es stacktop-1.

Después de copiar la función g() en la matriz a [] a continuación, establece el pasado en el valor de x para igualar el ret_addr, por lo que la pila es entonces:

stacktop: 1234 - main's a 
-1   ret_addr - the modified value of x 
-2   ret_addr - points to the printf in main, where f() will go when it's finished 
-3   a[99] 
      ... 

Se establece entonces (& x -1) a a []:

stacktop: 1234 - main's a 
-1   ret_addr - the modified value of x 
-2   &a[0] - points to the copy of g 
-3   a[99] 
      ... 

La función entonces sale. Esto mueve el puntero de la pila a stacktop-2, liberando los locals asignados (a [] en este caso), y luego salta a lo que está en la pila, en este caso & a [0] (stacktop-2), y disminuye tamaño de la pila.

Esto apunta a la copia de g(). g() se ejecuta y cuando luego sale, salta a la dirección en la parte superior de la pila (stacktop-1, en este caso ahora es el puntero a printf en main) y disminuye la pila nuevamente.

Esto tiene muchos problemas.

  1. Si la función g es mayor que los 100 bytes, obtendrá un desbordamiento del búfer.
  2. Si la función g contiene direcciones absolutas para codificar en g, por ejemplo, un salto condicional> 128 bytes, entonces la copia intentará saltar al g original.
  3. Si hay una interrupción al final de f() entre la liberación de variables locales y el salto a la dirección de retorno, la copia de g() podría estar dañada.
  4. En una compilación optimizada, el parámetro pasado a f() muy probablemente pase en un registro en lugar de en la pila, lo que arruina el orden de la dirección de retorno, es decir, se bloqueará.
  5. El sistema operativo puede evitar que se ejecuten los datos en la pila.
  6. Si está utilizando un sistema de memoria segmentada (es decir, 16bit 8086), el retorno de f() o g() no utilizará el segmento correcto y el programa se bloqueará.
  7. La pila puede crecer hacia arriba en algunas CPU.

Como regla general, no se meta con la pila y no copie el código.

1

Debería echarle un vistazo a este classic article, que explica el mecanismo del desbordamiento de la pila.

La comunicación con PC asume las órdenes del parámetro en la pila es:

[dirección ret] [x]

Así & x-1 es la dirección de la dirección de retorno

+0

interesante artículo, gracias por el enlace! – nubela