2012-09-12 11 views
10

En GCC puede utilizar un Goto calculado tomando la dirección de una etiqueta (como en void *addr = &&label) y luego saltar a la misma (jump *addr). El GCC manual dice que puede saltar a esta dirección desde cualquier ­ donde en la función, es solo que saltar a ella desde otra función no está definido.GCC Goto calculado y el valor del puntero de pila

Cuando se salta al código, no puede asumir nada sobre los valores de los registros, por lo que, presumiblemente, los vuelve a cargar desde la memoria. Sin embargo, el valor del puntero de la pila tampoco está necesariamente definido, por ejemplo, podría estar saltando de un ámbito anidado que declara variables adicionales.

La pregunta es cómo hace GCC para establecer el valor del puntero de la pila en el valor correcto (puede ser demasiado alto o demasiado bajo)? ¿Y cómo interactúa esto con (si es así)?

Finalmente, para obtener puntos extra, ¿cuáles son las limitaciones reales sobre dónde puede saltar a una etiqueta? Por ejemplo, ­ am ­, puede hacerlo desde un manejador de interrupciones.

Respuesta

11

En general, cuando tiene una función con etiquetas cuya dirección se toma, gcc necesita asegurarse de que puede saltar a esa etiqueta desde cualquier goto indirecto en la función, por lo que necesita diseñar la pila para que el el puntero de la pila no importa (todo está indexado fuera del puntero del marco), o que el puntero de la pila es consistente en todos ellos. Generalmente, esto significa que asigna una cantidad fija de espacio de pila cuando se inicia la función y nunca toca el puntero de pila después. Entonces, si tiene ámbitos internos con variables, el espacio se asignará al inicio de la función y se liberará al final de la función, no en el ámbito interno. Solo el constructor y el destructor (si los hay) deben estar vinculados al alcance interno.

La única restricción para saltar a las etiquetas es la que anotó; solo puede hacerlo desde la función que contiene las etiquetas. No desde ningún otro marco de pila de ninguna otra función o controlador de interrupción ni nada.

edición

Si usted quiere ser capaz de saltar de un marco de pila a otra, es necesario utilizar setjmp/longjmp o algo similar para desconectar la pila. Se puede combinar eso con un Goto indirecta - algo así como:

if (target = (void *)setjmp(jmpbuf)) goto *target; 

de esa manera se puede llamar desde cualquier longjmp(jmpbuf, label_address); función llamada para desenrollar la pila y luego salta a la etiqueta. Siempre que setjmp/longjmp funcione desde un manejador de interrupciones, esto también funcionará desde un manejador de interrupciones. También depende de sizeof(int) == sizeof(void *), que no siempre es el caso.

+1

+1, buena explicación – rkosegi

+0

Lo interesante de los controladores de interrupción es que la pila estará ilesa y puede restaurar todos los registros (incluida la sp). En este caso, siempre que gcc haga lo que usted describió, en realidad podría saltar a una etiqueta desde un manejador de interrupciones. El único caso en el que no funcionaría es si gcc pone un código de manejo de pila especial por función alrededor del goto calculado. – jleahy

+0

@jleahy: que sin duda podría (no sé si lo hace). El requisito mínimo es que la función tenga un estado "limpio" definido: todas las etiquetas cuya dirección se toma comienzan en este estado limpio, y todos los saltos calculados establecen el estado de limpieza antes de que salten. De hecho, podría ser incluso más mínima que eso: la "dirección de una etiqueta" podría ser la dirección de algún código que va desde el estado limpio al estado esperado en la etiqueta, luego 'goto' la etiqueta "real". Funcionaría, pero podría contradecir algo más que gcc dice sobre las direcciones de las etiquetas. –

0

En el prólogo de funciones, la posición actual de la pila se guarda en un registro guardado en línea incluso con -fomit-frame-pointer.

En el ejemplo siguiente, sp + 4 se almacena en r7 y luego en el epílogo (LBB0_3) se restaura (r7 + 4 -> r4; r4 -> sp). Debido a esto, puedes saltar a cualquier parte dentro de la función, hacer crecer la pila en cualquier punto de la función y no arruinar la pila. Si saltas de la función (a través de jump * addr) saltearás este epílogo y arruinarás la pila.

Short ejemplo que también utiliza alloca que asigna dinámicamente la memoria en la pila:

sonido metálico -arch ARMv7 -fomit-frame-pointer -c -S -O0 -O - pila.c

#include <alloca.h> 

int foo(int sz, int jmp) { 
    char *buf = alloca(sz); 
    int rval = 0; 

    if(jmp) { 
     rval = 1; 
     goto done; 
    } 

    volatile int s = 2; 

    rval = s * 5; 

done: 

    return rval; 
} 

y desmontaje:

_foo: 
@ BB#0: 
    push {r4, r7, lr} 
    add r7, sp, #4 
    sub sp, #20 
    movs r2, #0 
    movt r2, #0 
    str r0, [r7, #-8] 
    str r1, [r7, #-12] 
    ldr r0, [r7, #-8] 
    adds r0, #3 
    bic r0, r0, #3 
    mov r1, sp 
    subs r0, r1, r0 
    mov sp, r0 
    str r0, [r7, #-16] 
    str r2, [r7, #-20] 
    ldr r0, [r7, #-12] 
    cmp r0, #0 
    beq LBB0_2 
@ BB#1: 
    movs r0, #1 
    movt r0, #0 
    str r0, [r7, #-20] 
    b LBB0_3 
LBB0_2: 
    movs r0, #2 
    movt r0, #0 
    str r0, [r7, #-24] 
    ldr r0, [r7, #-24] 
    movs r1, #5 
    movt r1, #0 
    muls r0, r1, r0 
    str r0, [r7, #-20] 
LBB0_3: 
    ldr r0, [r7, #-20] 
    subs r4, r7, #4 
    mov sp, r4 
    pop {r4, r7, pc} 
+0

Esto es cierto para un goto normal, porque el compilador conoce el destino en tiempo de compilación, pero no estoy seguro de si es cierto para un goto calculado. – jleahy

+0

Sigue siendo cierto. El SP se guarda en un registro que no se manipula en ninguna otra parte de la función y se guarda/restaura mediante cualquier función que se llame. Por lo tanto, el código que fluye a través de la función (ya sea directamente o saltando dinámicamente) no importa siempre que pase por el epílogo de la función. – James

+2

'r7' es su puntero de marco aquí. El uso de alloca deshabilita -fomit-frame-puntero para la función como un puntero de marco es necesario para implementar alloca. –

2

No creo que el hecho de que las Goto se calculan de añadir al efecto que tiene sobre las variables locales. La vida útil de la variable local comienza desde su declaración en su declaración o más allá y finaliza cuando no se puede alcanzar el alcance de la variable de ninguna manera. Esto incluye todos los tipos diferentes de flujo de control, en particular goto y longjmp. Entonces, todas esas variables son siempre seguras, hasta el retorno de la función en la que se declaran.

Las etiquetas en C son visibles para toda la función de englobado, por lo que no hay mucha diferencia si se trata de un goto calculado. Siempre puede reemplazar un goto calculado con una declaración switch más o menos involucrada.

Una excepción notable de esta regla en variables locales son las matrices de longitud variable, VLA. Como hacen cambian necesariamente el puntero de pila, tienen reglas diferentes. La duración de la vida finaliza tan pronto como sale de su bloque de declaración y goto y longjmp no están permitidos en los ámbitos después de una declaración de un tipo modificado de forma variable.

Cuestiones relacionadas