Comprender los modelos de código de arquitectura x64

"¿Qué modelo de código debo usar?" - una pregunta que surge con frecuencia, pero que se discute con poca frecuencia al escribir código para la arquitectura x64. Sin embargo, este es un problema bastante interesante, y es útil tener una idea de los modelos de código para comprender el código de máquina x64 generado por los compiladores. Además, para aquellos que están preocupados por el rendimiento hasta las instrucciones más pequeñas, la elección del modelo de código también afecta la optimización.



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 manpá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 mainaccede 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;
}


gccusa el modelo de código como el valor de la opción -mcmodel. Además, la bandera -fpicse 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 movpara que corresponda a 0x3s. Estamos interesados ​​en la segunda reubicación, R_X86_64_PC32apunta al operando moven la dirección 0x38y 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 01significa "el séptimo int en la matriz" (en la arquitectura x64 el tamaño de cada uno intes 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_arraquí 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_arrpuede 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 callqtambién es relativo a RIP, la reubicación R_X86_64_PC32funciona 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 gccun 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 maincompilado 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) + 01dentro eax.



Así que vamos a centrarnos en el equipo 0x3d, movabsla versión absoluta de 64 bits moven 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_64del 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,raxcontendrá 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 movabssigue un comando callque 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 .datay .bss, se crean secciones especiales: .ldatay .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 _biga 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_arrranura reservada para el GOT. Por lo tanto, la dirección real se coloca raxen el comando en 0x36la dirección global_arr. Después de este paso se restablece el enlace a la dirección global_arrmá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 callqdirecció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 callqPLT 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_arrglobal_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 0x1ecarga su propia dirección en el rbx. Luego, junto con la reubicación, se realiza R_X86_64_GOTPC64un 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 0x2fagrega 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 rbxahora 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_arry 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_arrno 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_GOT64de movabssimplemente indica a la función para colocar el desplazamiento en el GOT donde el raxse encuentra la dirección global_arr. El comando en la dirección 0x5ftoma la dirección global_arrdel 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_funcdirecció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_funcat 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_funccompletamente similar al mismo código en el modelo PIC pequeño, así como para los casos de matrices de datos pequeñas static_arry 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_GOTPC32ingresar 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_biges 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_biges imposible obtener dicho acceso, pero este caso aún cubre el GOT, ya que en realidad está global_arr_bigen é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









All Articles