La información sobre este tema en línea, o en cualquier otro lugar, es rara. El más importante de los recursos disponibles es el x64 ABI oficial, puede descargarlo aquí (en adelante se denominará "ABI"). También se puede encontrar información en las
man
páginasgcc
. El objetivo de este artículo es proporcionar recomendaciones accesibles sobre el tema, discutir temas relacionados y también demostrar algunos conceptos a través del código utilizado en el trabajo con buenos ejemplos.
Nota importante: este artículo no es un tutorial para principiantes. Antes de familiarizarse, se recomienda que tenga un buen dominio de C y ensamblador, así como una familiaridad básica con la arquitectura x64.
Vea también nuestra publicación anterior sobre un tema similar: Cómo x86_x64 aborda la memoria
Modelos de código. Parte motivacional
En la arquitectura x64, tanto el código como los datos se referencian a través de modelos de direccionamiento relativos a comandos (o, usando jerga x64, relativos a RIP). En estos comandos, el cambio de RIP está limitado a 32 bits, sin embargo, puede haber casos en los que el equipo, al tratar de abordar parte de la memoria o los datos, simplemente no tiene un cambio de 32 bits, por ejemplo, cuando trabaja con programas de más de dos gigabytes.
Una forma de resolver este problema es abandonar por completo el modo de direccionamiento relativo a RIP a favor de un cambio completo de 64 bits para todas las referencias de datos y códigos. Sin embargo, este paso es muy costoso: para cubrir el caso (bastante raro) de programas y bibliotecas increíblemente grandes, incluso las operaciones más simples dentro del código completo requerirán más instrucciones de lo habitual.
Por lo tanto, los modelos de código se convierten en una compensación. [1] Un modelo de código es un acuerdo formal entre un programador y un compilador en el que un programador indica sus intenciones con respecto al tamaño del programa (o programas) esperado en el que caerá el módulo de objeto actualmente compilado. [2] Se necesitan modelos de código para que el programador pueda decirle al compilador: "no se preocupe, este módulo de objeto irá solo a programas pequeños, por lo que puede usar modos rápidos de direccionamiento relativos a RIP". Por otro lado, puede decirle al compilador lo siguiente: "vamos a vincular este módulo en programas grandes, así que utilice los modos de direccionamiento absoluto pausado y seguro con un cambio completo de 64 bits".
Lo que este artículo contará sobre
Hablaremos sobre los dos escenarios descritos anteriormente, el modelo de código pequeño y el modelo de código grande: el primer modelo le dice al compilador que un desplazamiento relativo de 32 bits debería ser suficiente para todas las referencias de código y datos en la unidad de objeto; el segundo insiste en que el compilador usa modos de direccionamiento absoluto de 64 bits. Además, también hay una versión intermedia, el llamado modelo de código medio .
Cada uno de estos modelos de código se presenta en variaciones independientes PIC y no PIC, y hablaremos de cada uno de los seis.
Ejemplo original de C
Para demostrar los conceptos discutidos en este artículo, usaré el siguiente programa C y lo compilaré con varios modelos de código. Como puede ver, la función
main
accede a cuatro matrices globales diferentes y una función global. Las matrices difieren en dos parámetros: tamaño y visibilidad. El tamaño es importante para explicar el modelo de código promedio y no será necesario cuando se trabaja con modelos pequeños y grandes. La visibilidad es importante para el funcionamiento de los modelos de código PIC y puede ser estática (visibilidad solo en el archivo fuente) o global (visibilidad de todos los objetos compilados en el programa).
int global_arr[100] = {2, 3};
static int static_arr[100] = {9, 7};
int global_arr_big[50000] = {5, 6};
static int static_arr_big[50000] = {10, 20};
int global_func(int param)
{
return param * 10;
}
int main(int argc, const char* argv[])
{
int t = global_func(argc);
t += global_arr[7];
t += static_arr[7];
t += global_arr_big[7];
t += static_arr_big[7];
return t;
}
gcc
usa el modelo de código como el valor de la opción -mcmodel
. Además, la bandera -fpic
se puede utilizar para configurar la compilación PIC.
Un ejemplo de compilación a un módulo de objeto a través de un modelo de código grande usando PIC:
> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o
Modelo de código pequeño
Traducción de una cita de man gcc en el modelo de código pequeño:
-mcmodel =
generación de código pequeño para un modelo pequeño: el programa y sus símbolos deben estar vinculados en los dos gigabytes inferiores del espacio de direcciones. Los punteros tienen un tamaño de 64 bits. Los programas pueden vincularse estática o dinámicamente. Este es el modelo de código básico.
En otras palabras, el compilador puede asumir con seguridad que se puede acceder al código y los datos a través de un cambio relativo de RIP de 32 bits desde cualquier comando en el código. Echemos un vistazo a un ejemplo desmontado de un programa C que compilamos a través de un modelo de código pequeño que no es PIC:
> objdump -dS codemodel1_small.o
[...]
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
3f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
45: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
48: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
4e: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
51: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
57: 01 45 fc add %eax,-0x4(%rbp)
return t;
5a: 8b 45 fc mov -0x4(%rbp),%eax
}
5d: c9 leaveq
5e: c3 retq
Como puede ver, el acceso a todos los arreglos se organiza de la misma manera, utilizando el desplazamiento relativo a RIP. Sin embargo, en el código, el cambio es 0, ya que el compilador no sabe dónde se colocará el segmento de datos, por lo que para cada acceso, crea una reubicación:
> readelf -r codemodel1_small.o
Relocation section '.rela.text' at offset 0x62bd8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000002f 001500000002 R_X86_64_PC32 0000000000000000 global_func - 4
000000000038 001100000002 R_X86_64_PC32 0000000000000000 global_arr + 18
000000000041 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
00000000004a 001200000002 R_X86_64_PC32 0000000000000340 global_arr_big + 18
000000000053 000300000002 R_X86_64_PC32 0000000000000000 .data + 31098
Vamos a decodificar completamente el acceso a
global_arr
. El segmento desmontado que nos interesa:
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
El direccionamiento relativo a RIP es relativo al siguiente comando, por lo que el cambio debe ser parcheado al comando
mov
para que corresponda a 0x3s. Estamos interesados en la segunda reubicación, R_X86_64_PC32
apunta al operando mov
en la dirección 0x38
y significa lo siguiente: tomamos el valor del símbolo, sumamos el término y restamos el turno indicado por la reubicación. Si ha calculado todo correctamente, verá cómo el resultado colocará un cambio relativo entre el siguiente comando y global_arr
, más 01
. Como 01
significa "el séptimo int en la matriz" (en la arquitectura x64 el tamaño de cada uno int
es de 4 bytes), entonces necesitamos este cambio relativo. Por lo tanto, utilizando el direccionamiento relativo a RIP, el comando hace referencia correctamente global_arr[7]
.
También es interesante observar lo siguiente: aunque los comandos de acceso
static_arr
aquí son similares, su redirección utiliza un símbolo diferente, por lo que apunta a una sección en lugar de un símbolo específico .data
. Esto se debe a las acciones del enlazador, coloca la matriz estática en una ubicación conocida en la sección y, por lo tanto, la matriz no se puede compartir con otras bibliotecas compartidas. Como resultado, el vinculador resolverá la situación con esta reubicación. Por otro lado, dado que global_arr
puede ser utilizado (o sobrescrito) por otra biblioteca compartida, el cargador dinámico ya tendrá que descifrar el enlace global_arr
. [3]
Finalmente, echemos un vistazo a la referencia a
global_func
:
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
Dado que el operando
callq
también es relativo a RIP, la reubicación R_X86_64_PC32
funciona aquí de la misma manera que colocando el desplazamiento relativo real a global_func en el operando.
En conclusión, observamos que debido al modelo de código pequeño, el compilador percibe todos los datos y códigos del futuro programa como disponibles a través de un cambio de 32 bits, y por lo tanto crea un código simple y eficiente para acceder a todo tipo de objetos.
Modelo de código grande
Traducción de una cita de
man
gcc
un modelo de código grande:
-mcmodel = grande
Generando código para un modelo grande: este modelo no hace suposiciones sobre direcciones y tamaños de sección.
Un ejemplo de código desmontado
main
compilado usando un modelo grande que no es PIC:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
35: 00 00 00
38: ff d2 callq *%rdx
3a: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
3d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
44: 00 00 00
47: 8b 40 1c mov 0x1c(%rax),%eax
4a: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
4d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
54: 00 00 00
57: 8b 40 1c mov 0x1c(%rax),%eax
5a: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
5d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
64: 00 00 00
67: 8b 40 1c mov 0x1c(%rax),%eax
6a: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
6d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
74: 00 00 00
77: 8b 40 1c mov 0x1c(%rax),%eax
7a: 01 45 fc add %eax,-0x4(%rbp)
return t;
7d: 8b 45 fc mov -0x4(%rbp),%eax
}
80: c9 leaveq
81: c3 retq
Nuevamente, es útil observar las reubicaciones:
Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000030 001500000001 R_X86_64_64 0000000000000000 global_func + 0
00000000003f 001100000001 R_X86_64_64 0000000000000000 global_arr + 0
00000000004f 000300000001 R_X86_64_64 0000000000000000 .data + 1a0
00000000005f 001200000001 R_X86_64_64 0000000000000340 global_arr_big + 0
00000000006f 000300000001 R_X86_64_64 0000000000000000 .data + 31080
Dado que no es necesario hacer suposiciones sobre el tamaño de las secciones de código y los datos, el modelo de código grande está bastante unificado e identifica el acceso a todos los datos de la misma manera. Echemos otro vistazo a
global_arr
:
t += global_arr[7];
3d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
44: 00 00 00
47: 8b 40 1c mov 0x1c(%rax),%eax
4a: 01 45 fc add %eax,-0x4(%rbp)
Dos comandos necesitan obtener el valor deseado de la matriz. El primer comando coloca una dirección absoluta de 64 bits
rax
, que, como veremos en breve, resulta ser una dirección global_arr
, mientras que el segundo comando carga una palabra desde (rax) + 01
dentro eax
.
Así que vamos a centrarnos en el equipo
0x3d
, movabs
la versión absoluta de 64 bits mov
en la arquitectura x64. Puede soltar la constante completa de 64 bits directamente en el registro, y dado que en nuestro código desmontado el valor de esta constante es cero, tendremos que consultar la tabla de reubicación para obtener la respuesta. En él, encontraremos la reubicación absoluta R_X86_64_64
del operando en la dirección 0x3f
, con el siguiente valor: colocar el valor del símbolo más el sumando nuevamente en el turno. En otras palabras,rax
contendrá una dirección absoluta global_arr
.
¿Qué pasa con la función de llamada?
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
35: 00 00 00
38: ff d2 callq *%rdx
3a: 89 45 fc mov %eax,-0x4(%rbp)
Al que ya conocemos le
movabs
sigue un comando call
que llama a una función en la dirección en rdx
. Simplemente mire la reubicación correspondiente para ver qué tan similar es acceder a los datos.
Como puede ver, en el modelo de código grande no hay suposiciones sobre el tamaño del código y las secciones de datos, así como sobre la disposición final de los caracteres, simplemente se refiere a los caracteres a través de pasos absolutos de 64 bits, una especie de "pista segura". Sin embargo, observe cómo, en comparación con un modelo de código pequeño, un modelo grande se ve obligado a usar un comando adicional al acceder a cada carácter. Este es el precio de la seguridad.
Entonces, nos encontramos con dos modelos completamente opuestos: mientras que el modelo de código pequeño supone que todo encaja en los dos gigabytes de memoria inferiores, el modelo grande supone que nada es imposible y que cualquier personaje puede estar en cualquier lugar en su totalidad 64- poco espacio de direcciones. La compensación entre los dos es el modelo de código medio.
Modelo de código medio
Como antes, echemos un vistazo a la traducción de la cita de
man
gcc
:
-mcmodel=medium
: . . , -mlarge-data-threshold, bss . , .
Similar al modelo de código pequeño, el modelo mediano supone que todo el código se compila en los dos gigabytes inferiores. Sin embargo, los datos se dividen en supuestamente dispuestos en la parte inferior dos gigabytes de "datos pequeños" e ilimitados en el espacio de memoria "big data". Los datos se clasifican como grandes cuando exceden el límite, que es, por definición, 64 kilobytes.
También es importante tener en cuenta que cuando se trabaja con un modelo de código promedio para big data, por analogía con las secciones
.data
y .bss
, se crean secciones especiales: .ldata
y .lbss
. Esto no es tan importante en el prisma del tema del artículo actual, pero me desviaré un poco. Se pueden encontrar más detalles sobre este tema en el ABI.
Ahora queda claro por qué esas matrices aparecieron en el ejemplo
_big
: el modelo promedio los necesita para interpretar los "big data" que son, a 200 kilobytes cada uno. A continuación puede ver el resultado del desmontaje:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
3f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
45: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
48: 48 b8 00 00 00 00 00 movabs $0x0,%rax
4f: 00 00 00
52: 8b 40 1c mov 0x1c(%rax),%eax
55: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
58: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5f: 00 00 00
62: 8b 40 1c mov 0x1c(%rax),%eax
65: 01 45 fc add %eax,-0x4(%rbp)
return t;
68: 8b 45 fc mov -0x4(%rbp),%eax
}
6b: c9 leaveq
6c: c3 retq
Preste atención a cómo se accede a las matrices: se accede a las matrices
_big
a través de los métodos del modelo de código grande, mientras que al resto de las matrices se accede a través de los métodos del modelo pequeño. La función también se llama utilizando el método del modelo de código pequeño, y las reubicaciones son tan similares a los ejemplos anteriores que ni siquiera las demostraré.
El modelo de código medio es un intercambio hábil entre modelos grandes y pequeños. Es poco probable que el código del programa resulte ser demasiado grande [4], por lo que solo grandes fragmentos de datos enlazados estáticamente pueden moverlo más allá del límite de dos gigabytes, tal vez como parte de algún tipo de búsqueda de tabla voluminosa. Dado que el modelo de código intermedio filtra datos tan grandes y los procesa de manera especial, las llamadas por código de funciones y símbolos pequeños serán tan eficientes como en el modelo de código pequeño. Solo los accesos a símbolos grandes, por analogía con el modelo grande, requerirán que el código use el método completo de 64 bits del modelo grande.
Modelo de código PIC pequeño
Ahora echemos un vistazo a las variantes PIC de los modelos de código y, como antes, comenzamos con el modelo pequeño. [5] A continuación puede ver un ejemplo del código compilado a través del pequeño modelo PIC:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
3d: 8b 40 1c mov 0x1c(%rax),%eax
40: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
43: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
49: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
4c: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
53: 8b 40 1c mov 0x1c(%rax),%eax
56: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
59: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
5f: 01 45 fc add %eax,-0x4(%rbp)
return t;
62: 8b 45 fc mov -0x4(%rbp),%eax
}
65: c9 leaveq
66: c3 retq
Reubicaciones:
Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000002f 001600000004 R_X86_64_PLT32 0000000000000000 global_func - 4
000000000039 001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
000000000045 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
00000000004f 001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 4
00000000005b 000300000002 R_X86_64_PC32 0000000000000000 .data + 31098
Dado que las diferencias entre los datos grandes y pequeños no juegan ningún papel en el modelo de código pequeño, nos centraremos en los puntos importantes al generar código a través de PIC: las diferencias entre símbolos locales (estáticos) y globales.
Como puede ver, no hay diferencia entre el código generado para las matrices estáticas y el código en el caso no PIC. Esta es una de las ventajas de la arquitectura x64: gracias al acceso relativo a IP a los datos, obtenemos un PIC como bonificación, al menos hasta que se requiera acceso externo a los símbolos. Todos los comandos y reubicaciones permanecen iguales, por lo que no hay necesidad de procesarlos nuevamente.
Es interesante prestar atención a las matrices globales: vale la pena recordar que en PIC los datos globales deben pasar por el GOT, porque en algún momento pueden ser almacenados o compartidos por bibliotecas compartidas [6]. A continuación puede ver el código para acceder
global_arr
:
t += global_arr[7];
36: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
3d: 8b 40 1c mov 0x1c(%rax),%eax
40: 01 45 fc add %eax,-0x4(%rbp)
La reubicación de interés para nosotros es
R_X86_64_GOTPCREL
: la posición de la entrada del símbolo en el GOT más el término, menos el turno para aplicar la reubicación. En otras palabras, el comando está reparando el desplazamiento relativo entre el RIP (siguiente instrucción) y la global_arr
ranura reservada para el GOT. Por lo tanto, la dirección real se coloca rax
en el comando en 0x36
la dirección global_arr
. Después de este paso se restablece el enlace a la dirección global_arr
más un cambio a su séptimo elemento en eax
.
Ahora echemos un vistazo a la llamada a la función:
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
Tiene una reubicación del operando
callq
dirección 0x2e
, R_X86_64_PLT32
: la dirección de entrada PLT para el cambio negativo símbolo más plazo para la solicitud de reubicación. En otras palabras, el callq
PLT debe llamar correctamente al trampolín global_func
.
Tenga en cuenta las suposiciones implícitas que hace el compilador: que se puede acceder a GOT y PLT a través del direccionamiento relativo a RIP. Esto será importante al comparar este modelo con otras variantes del modelo de código PIC.
Modelo de código PIC grande
Desmontaje
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 53 push %rbx
1a: 48 83 ec 28 sub $0x28,%rsp
1e: 48 8d 1d f9 ff ff ff lea -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00 movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db add %r11,%rbx
32: 89 7d dc mov %edi,-0x24(%rbp)
35: 48 89 75 d0 mov %rsi,-0x30(%rbp)
int t = global_func(argc);
39: 8b 45 dc mov -0x24(%rbp),%eax
3c: 89 c7 mov %eax,%edi
3e: b8 00 00 00 00 mov $0x0,%eax
43: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da add %rbx,%rdx
50: ff d2 callq *%rdx
52: 89 45 ec mov %eax,-0x14(%rbp)
t += global_arr[7];
55: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5c: 00 00 00
5f: 48 8b 04 03 mov (%rbx,%rax,1),%rax
63: 8b 40 1c mov 0x1c(%rax),%eax
66: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr[7];
69: 48 b8 00 00 00 00 00 movabs $0x0,%rax
70: 00 00 00
73: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
77: 01 45 ec add %eax,-0x14(%rbp)
t += global_arr_big[7];
7a: 48 b8 00 00 00 00 00 movabs $0x0,%rax
81: 00 00 00
84: 48 8b 04 03 mov (%rbx,%rax,1),%rax
88: 8b 40 1c mov 0x1c(%rax),%eax
8b: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr_big[7];
8e: 48 b8 00 00 00 00 00 movabs $0x0,%rax
95: 00 00 00
98: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
9c: 01 45 ec add %eax,-0x14(%rbp)
return t;
9f: 8b 45 ec mov -0x14(%rbp),%eax
}
a2: 48 83 c4 28 add $0x28,%rsp
a6: 5b pop %rbx
a7: c9 leaveq
a8: c3 retq
Reubicaciones: esta vez, las diferencias entre los datos grandes y pequeños aún no importan, por lo que nos centraremos en y . Pero primero debe prestar atención al prólogo en este código, anteriormente no encontramos esto:
Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9
000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0
000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0
00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0
00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0
000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0
static_arr
global_arr
1e: 48 8d 1d f9 ff ff ff lea -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00 movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db add %r11,%rbx
A continuación puede leer la traducción de la cita relacionada del ABI:
( GOT) AMD64 IP- . GOT . GOT , AMD64 ISA 32 .
Echemos un vistazo a cómo el prólogo descrito anteriormente calcula la dirección GOT. Primero, el comando en la dirección
0x1e
carga su propia dirección en el rbx
. Luego, junto con la reubicación, se realiza R_X86_64_GOTPC64
un paso absoluto de 64 bits r11
. Esta reubicación significa lo siguiente: tome la dirección del GOT, reste el turno desplazado y agregue el término. Finalmente, el comando en dirección 0x2f
agrega ambos resultados juntos. El resultado es la dirección absoluta de GOT rbx
. [7]
¿Por qué molestarse en calcular la dirección GOT? Primero, como se señala en la cita, en un modelo de código grande, no podemos suponer que un cambio relativo a RIP de 32 bits será suficiente para el direccionamiento GOT, por lo que necesitamos una dirección completa de 64 bits. En segundo lugar, todavía queremos trabajar con la variación PIC, por lo que no podemos simplemente poner la dirección absoluta en el registro. Más bien, la dirección en sí misma debe calcularse en relación con el RIP. Para esto, necesitamos un prólogo: realiza un cálculo relativo a RIP de 64 bits.
De todos modos, dado que
rbx
ahora tenemos una dirección GOT, echemos un vistazo a cómo acceder static_arr
:
t += static_arr[7];
69: 48 b8 00 00 00 00 00 movabs $0x0,%rax
70: 00 00 00
73: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
77: 01 45 ec add %eax,-0x14(%rbp)
La reubicación del primer comando es
R_X86_64_GOTOFF64
: el símbolo más el término menos GOT. En nuestro caso, este es el desplazamiento relativo entre la dirección static_arr
y la dirección GOT. La siguiente instrucción agrega el resultado a rbx
(dirección GOT absoluta) y restablece el desplazamiento por referencia 0x1c
. Para facilitar la visualización de dicho cálculo, a continuación puede ver el ejemplo de pseudo-C:
// char* static_arr
// char* GOT
rax = static_arr + 0 - GOT; // rax now contains an offset
eax = *(rbx + rax + 0x1c); // rbx == GOT, so eax now contains
// *(GOT + static_arr - GOT + 0x1c) or
// *(static_arr + 0x1c)
Tenga en cuenta un punto interesante: la dirección GOT se utiliza como enlace para
static_arr
. Por lo general, un GOT no contiene una dirección de símbolo, y dado que static_arr
no es un símbolo externo, no hay razón para almacenarlo dentro de un GOT. Sin embargo, en este caso, el GOT se utiliza como enlace a la dirección de símbolo relativa de la sección de datos. Esta dirección, que, entre otras cosas, es independiente de la ubicación, se puede encontrar con un desplazamiento completo de 64 bits. El enlazador puede manejar esta reubicación, por lo que no es necesario modificar la sección de código en el momento de la carga.
¿Pero que pasa
global_arr
?
t += global_arr[7];
55: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5c: 00 00 00
5f: 48 8b 04 03 mov (%rbx,%rax,1),%rax
63: 8b 40 1c mov 0x1c(%rax),%eax
66: 01 45 ec add %eax,-0x14(%rbp)
Este código es algo más largo y la reubicación es diferente de la habitual. De hecho, el GOT se utiliza aquí de una manera más tradicional: la reubicación
R_X86_64_GOT64
de movabs
simplemente indica a la función para colocar el desplazamiento en el GOT donde el rax
se encuentra la dirección global_arr
. El comando en la dirección 0x5f
toma la dirección global_arr
del GOT y la coloca rax
. El siguiente comando restablece la referencia global_arr[7]
y pone el valor en eax
.
Ahora echemos un vistazo al enlace de código para
global_func
. Recuerde que en un modelo de código grande, no podríamos hacer suposiciones sobre el tamaño de las secciones de código, por lo que debemos suponer que incluso para acceder al PLT necesitamos una dirección absoluta de 64 bits:
int t = global_func(argc);
39: 8b 45 dc mov -0x24(%rbp),%eax
3c: 89 c7 mov %eax,%edi
3e: b8 00 00 00 00 mov $0x0,%eax
43: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da add %rbx,%rdx
50: ff d2 callq *%rdx
52: 89 45 ec mov %eax,-0x14(%rbp)
La reubicación que nos interesa es
R_X86_64_PLTOFF64
: la global_func
dirección de entrada PLT para menos la dirección GOT. El resultado se coloca en rdx
, donde luego se coloca rbx
(dirección absoluta del GOT). Como resultado, obtenemos la dirección PLT de entrada para global_func
at rdx
.
Tenga en cuenta que nuevamente GOT se usa como un ancla, esta vez para proporcionar una referencia independiente de la dirección al desplazamiento de la entrada PLT.
Modelo de código PIC promedio
Finalmente, desglosaremos el código generado para el modelo PIC promedio:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 53 push %rbx
1a: 48 83 ec 28 sub $0x28,%rsp
1e: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx
25: 89 7d dc mov %edi,-0x24(%rbp)
28: 48 89 75 d0 mov %rsi,-0x30(%rbp)
int t = global_func(argc);
2c: 8b 45 dc mov -0x24(%rbp),%eax
2f: 89 c7 mov %eax,%edi
31: b8 00 00 00 00 mov $0x0,%eax
36: e8 00 00 00 00 callq 3b <main+0x26>
3b: 89 45 ec mov %eax,-0x14(%rbp)
t += global_arr[7];
3e: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
45: 8b 40 1c mov 0x1c(%rax),%eax
48: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr[7];
4b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
51: 01 45 ec add %eax,-0x14(%rbp)
t += global_arr_big[7];
54: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
5b: 8b 40 1c mov 0x1c(%rax),%eax
5e: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr_big[7];
61: 48 b8 00 00 00 00 00 movabs $0x0,%rax
68: 00 00 00
6b: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
6f: 01 45 ec add %eax,-0x14(%rbp)
return t;
72: 8b 45 ec mov -0x14(%rbp),%eax
}
75: 48 83 c4 28 add $0x28,%rsp
79: 5b pop %rbx
7a: c9 leaveq
7b: c3 retq
Reubicaciones:
Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000021 00160000001a R_X86_64_GOTPC32 0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4
000000000037 001700000004 R_X86_64_PLT32 0000000000000000 global_func - 4
000000000041 001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
00000000004d 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
000000000057 001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4
000000000063 000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0
Primero, eliminemos la llamada a la función. Similar al modelo pequeño, en el modelo medio suponemos que las referencias de código no exceden los límites del cambio RIP de 32 bits, por lo tanto, el código para la llamada es
global_func
completamente similar al mismo código en el modelo PIC pequeño, así como para los casos de matrices de datos pequeñas static_arr
y global_arr
. Por lo tanto, nos centraremos en las matrices de big data, pero primero hablemos del prólogo: aquí difiere del prólogo del modelo de big data.
1e: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx
Este es todo el prólogo: para utilizar la reubicación para
R_X86_64_GOTPC32
ingresar la dirección GOT rbx
, solo se necesitó un equipo (en comparación con tres en el modelo grande). ¿Cual es la diferencia? El punto es que, dado que en el modelo intermedio el GOT no es parte de las "particiones de big data", asumimos su disponibilidad dentro de un cambio de 32 bits. En el modelo grande, no pudimos hacer tales suposiciones, y tuvimos que usar el desplazamiento completo de 64 bits.
Es interesante el hecho de que el código de acceso
global_arr_big
es similar al mismo código en el modelo PIC pequeño. Esto se debe a la misma razón por la que el prólogo del modelo intermedio es más corto que el prólogo del modelo grande: suponemos que el GOT está disponible dentro del direccionamiento relativo RIP de 32 bits. De hecho, para míglobal_arr_big
es imposible obtener dicho acceso, pero este caso aún cubre el GOT, ya que en realidad está global_arr_big
en él, además, en forma de una dirección completa de 64 bits.
La situación, sin embargo, es diferente para
static_arr_big
:
t += static_arr_big[7];
61: 48 b8 00 00 00 00 00 movabs $0x0,%rax
68: 00 00 00
6b: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
6f: 01 45 ec add %eax,-0x14(%rbp)
Este caso es similar al modelo de código PIC grande, ya que aquí todavía obtenemos la dirección absoluta del símbolo, que no está en el GOT. Dado que este es un carácter grande, que no se puede suponer que está en los dos gigabytes inferiores, nosotros, como en el modelo grande, requerimos un cambio PIC de 64 bits.
Notas:
[1] No confunda los modelos de código con los modelos de datos de 64 bits y los modelos de memoria Intel , estos son temas diferentes.
[2] Es importante recordar: los comandos reales son creados por el compilador, y los modos de direccionamiento se fijan exactamente en este paso. El compilador no puede saber en qué programas o bibliotecas compartidas caerá el módulo de objeto, algunos pueden ser pequeños, mientras que otros pueden ser grandes. El vinculador conoce el tamaño del programa final, pero es demasiado tarde: el vinculador solo puede parchear el cambio de comandos con la reubicación, y no cambiar los comandos por sí mismos. Por lo tanto, el "acuerdo" del modelo de código debe ser "firmado" por el programador en la etapa de compilación.
[3] Si algo no está claro, mira el siguiente artículo .
[4] Sin embargo, los volúmenes están aumentando gradualmente. La última vez que revisé la compilación Clang Debug + Asserts, casi alcanzó un gigabyte, por lo que muchas gracias al código generado automáticamente.
[5] Si aún no sabe cómo funciona PIC (tanto en general como en particular para la arquitectura x64), es hora de leer los siguientes artículos sobre el tema: una y dos veces .
[6] Por lo tanto, el enlazador no puede resolver los enlaces por sí solo, y tiene que cambiar el procesamiento GOT al cargador dinámico.
[7] 0x25 - 0x7 + GOT - 0x27 + 0x9 = GOT