Variables de registro explícito
https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Explicit-Register-Variables.html#Explicit-Reg-Vars)
Creo que, en general, este debería ser el enfoque recomendado sobre las restricciones de registro porque:
- puede representar todos los registros, incluido
r8
,r9
yr10
que se utilizan para los argumentos de llamada al sistema:¿Cómo especificar restricciones de registro en el registro Intel x86_64 r8 a r15 en el ensamblaje en línea GCC? - es la única opción óptima para otras ISA además de x86 como ARM, que no tienen los nombres de restricción de registro mágico:¿Cómo especificar un registro individual como restricción en el ensamblado en línea ARM GCC? (además de usar un registro temporal + clobbers + y una instrucción de movimiento adicional)
- Argumentaré que esta sintaxis es más legible que usar mnemónicos de una sola letra como
S -> rsi
Las variables de registro se utilizan, por ejemplo, en glibc 2.29, consulte:sysdeps/unix/sysv/linux/x86_64/sysdep.h
.
main_reg.c
#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size) {
register int64_t rax __asm__ ("rax") = 1;
register int rdi __asm__ ("rdi") = fd;
register const void *rsi __asm__ ("rsi") = buf;
register size_t rdx __asm__ ("rdx") = size;
__asm__ __volatile__ (
"syscall"
: "+r" (rax)
: "r" (rdi), "r" (rsi), "r" (rdx)
: "rcx", "r11", "memory"
);
return rax;
}
void my_exit(int exit_status) {
register int64_t rax __asm__ ("rax") = 60;
register int rdi __asm__ ("rdi") = exit_status;
__asm__ __volatile__ (
"syscall"
: "+r" (rax)
: "r" (rdi)
: "rcx", "r11", "memory"
);
}
void _start(void) {
char msg[] = "hello world\n";
my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}
GitHub ascendente.
Compilar y ejecutar:
gcc -O3 -std=c99 -ggdb3 -ffreestanding -nostdlib -Wall -Werror \
-pedantic -o main_reg.out main_reg.c
./main.out
echo $?
Salida
hello world
0
A modo de comparación, lo siguiente es análogo a ¿Cómo invocar una llamada al sistema a través de syscall o sysenter en el ensamblado en línea? produce un ensamblaje equivalente:
restricción_principal.c
#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size) {
ssize_t ret;
__asm__ __volatile__ (
"syscall"
: "=a" (ret)
: "0" (1), "D" (fd), "S" (buf), "d" (size)
: "rcx", "r11", "memory"
);
return ret;
}
void my_exit(int exit_status) {
ssize_t ret;
__asm__ __volatile__ (
"syscall"
: "=a" (ret)
: "0" (60), "D" (exit_status)
: "rcx", "r11", "memory"
);
}
void _start(void) {
char msg[] = "hello world\n";
my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}
GitHub ascendente.
Desmontaje de ambos con:
objdump -d main_reg.out
es casi idéntico, aquí está el main_reg.c
uno:
Disassembly of section .text:
0000000000001000 <my_write>:
1000: b8 01 00 00 00 mov $0x1,%eax
1005: 0f 05 syscall
1007: c3 retq
1008: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
100f: 00
0000000000001010 <my_exit>:
1010: b8 3c 00 00 00 mov $0x3c,%eax
1015: 0f 05 syscall
1017: c3 retq
1018: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
101f: 00
0000000000001020 <_start>:
1020: c6 44 24 ff 00 movb $0x0,-0x1(%rsp)
1025: bf 01 00 00 00 mov $0x1,%edi
102a: 48 8d 74 24 f3 lea -0xd(%rsp),%rsi
102f: 48 b8 68 65 6c 6c 6f movabs $0x6f77206f6c6c6568,%rax
1036: 20 77 6f
1039: 48 89 44 24 f3 mov %rax,-0xd(%rsp)
103e: ba 0d 00 00 00 mov $0xd,%edx
1043: b8 01 00 00 00 mov $0x1,%eax
1048: c7 44 24 fb 72 6c 64 movl $0xa646c72,-0x5(%rsp)
104f: 0a
1050: 0f 05 syscall
1052: 31 ff xor %edi,%edi
1054: 48 83 f8 0d cmp $0xd,%rax
1058: b8 3c 00 00 00 mov $0x3c,%eax
105d: 40 0f 95 c7 setne %dil
1061: 0f 05 syscall
1063: c3 retq
Entonces vemos que GCC incorporó esas pequeñas funciones de llamada al sistema como se desearía.
my_write
y my_exit
son iguales para ambos, pero _start
en main_constraint.c
es ligeramente diferente:
0000000000001020 <_start>:
1020: c6 44 24 ff 00 movb $0x0,-0x1(%rsp)
1025: 48 8d 74 24 f3 lea -0xd(%rsp),%rsi
102a: ba 0d 00 00 00 mov $0xd,%edx
102f: 48 b8 68 65 6c 6c 6f movabs $0x6f77206f6c6c6568,%rax
1036: 20 77 6f
1039: 48 89 44 24 f3 mov %rax,-0xd(%rsp)
103e: b8 01 00 00 00 mov $0x1,%eax
1043: c7 44 24 fb 72 6c 64 movl $0xa646c72,-0x5(%rsp)
104a: 0a
104b: 89 c7 mov %eax,%edi
104d: 0f 05 syscall
104f: 31 ff xor %edi,%edi
1051: 48 83 f8 0d cmp $0xd,%rax
1055: b8 3c 00 00 00 mov $0x3c,%eax
105a: 40 0f 95 c7 setne %dil
105e: 0f 05 syscall
1060: c3 retq
Es interesante observar que, en este caso, GCC encontró una codificación equivalente ligeramente más corta seleccionando:
104b: 89 c7 mov %eax,%edi
para configurar el fd
al 1
, que es igual a 1
desde el número de llamada al sistema, en lugar de un más directo:
1025: bf 01 00 00 00 mov $0x1,%edi
Para obtener una discusión detallada de las convenciones de llamadas, consulte también:¿Cuáles son las convenciones de llamadas para las llamadas del sistema UNIX y Linux (y funciones de espacio de usuario) en i386 y x86-64?
Probado en Ubuntu 18.10, GCC 8.2.0.
En primer lugar, no puedes usar GNU C Basic asm("");
de forma segura sintaxis para esto (sin restricciones de entrada/salida/golpe). Necesita Extended asm para informarle al compilador sobre los registros que modifica. Consulte el asm en línea en el manual de GNU C y la wiki de etiquetas de ensamblaje en línea para obtener enlaces a otras guías para obtener detalles sobre cosas como "D"(1)
significa como parte de un asm()
declaración.
También necesitas asm volatile
porque eso no está implícito para Extended asm
sentencias con 1 o más operandos de salida.
Voy a mostrarte cómo ejecutar llamadas al sistema escribiendo un programa que escriba Hello World!
a la salida estándar usando el write()
llamada del sistema. Aquí está la fuente del programa sin una implementación de la llamada al sistema real:
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size);
int main(void)
{
const char hello[] = "Hello world!\n";
my_write(1, hello, sizeof(hello));
return 0;
}
Puede ver que nombré mi función personalizada de llamada al sistema como my_write
para evitar conflictos de nombres con el write
"normal" , proporcionado por libc. El resto de esta respuesta contiene la fuente de my_write
para i386 y amd64.
i386
Las llamadas al sistema en i386 Linux se implementan utilizando el vector de interrupción 128, p. llamando al int 0x80
en su código ensamblador, habiendo establecido los parámetros en consecuencia de antemano, por supuesto. Es posible hacer lo mismo a través de SYSENTER
, pero la ejecución real de esta instrucción se logra mediante el VDSO virtualmente asignado a cada proceso en ejecución. Desde SYSENTER
nunca se pensó como un reemplazo directo del int 0x80
API, nunca es ejecutado directamente por las aplicaciones de la zona del usuario; en cambio, cuando una aplicación necesita acceder a algún código del kernel, llama a la rutina asignada virtualmente en el VDSO (eso es lo que el call *%gs:0x10
en su código es para), que contiene todo el código compatible con SYSENTER
instrucción. Hay mucho debido a cómo funciona realmente la instrucción.
Si quieres leer más sobre esto, echa un vistazo a este enlace. Contiene una descripción bastante breve de las técnicas aplicadas en el kernel y el VDSO. Consulte también La guía definitiva de llamadas al sistema Linux (x86):algunas llamadas al sistema como getpid
y clock_gettime
son tan simples que el kernel puede exportar código + datos que se ejecutan en el espacio del usuario, por lo que el VDSO nunca necesita ingresar al kernel, lo que lo hace mucho más rápido incluso que sysenter
podría ser.
Es mucho más fácil usar el int $0x80
más lento para invocar la ABI de 32 bits.
// i386 Linux
#include <asm/unistd.h> // compile with -m32 for 32 bit call numbers
//#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"int $0x80"
: "=a" (ret)
: "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
: "memory" // the kernel dereferences pointer args
);
return ret;
}
Como puede ver, usando el int 0x80
La API es relativamente simple. El número de la llamada al sistema va al eax
registrarse, mientras que todos los parámetros necesarios para la llamada al sistema van respectivamente a ebx
, ecx
, edx
, esi
, edi
y ebp
. Los números de llamada del sistema se pueden obtener leyendo el archivo /usr/include/asm/unistd_32.h
.
Los prototipos y las descripciones de las funciones están disponibles en la segunda sección del manual, por lo que en este caso write(2)
.
El kernel guarda/restaura todos los registros (excepto EAX) para que podamos usarlos como operandos de solo entrada para el asm en línea. Consulte ¿Cuáles son las convenciones de llamada para las llamadas del sistema UNIX y Linux (y funciones de espacio de usuario) en i386 y x86-64?
Tenga en cuenta que la lista de clobber también contiene el memory
parámetro, lo que significa que la instrucción enumerada en la lista de instrucciones hace referencia a la memoria (a través del buf
parámetro). (Una entrada de puntero a ASM en línea no implica que la memoria apuntada también sea una entrada. Consulte ¿Cómo puedo indicar que se puede usar la memoria *apuntada* por un argumento ASM en línea?)
amd64
Las cosas se ven diferentes en la arquitectura AMD64 que tiene una nueva instrucción llamada SYSCALL
. Es muy diferente del SYSENTER
original instrucción, y definitivamente mucho más fácil de usar desde aplicaciones de usuario - realmente se parece a un CALL
normal , en realidad, y adaptando el antiguo int 0x80
al nuevo SYSCALL
es bastante trivial. (Excepto que usa RCX y R11 en lugar de la pila del kernel para guardar el RIP y RFLAGS del espacio de usuario para que el kernel sepa a dónde regresar).
En este caso, el número de la llamada al sistema aún se pasa en el registro rax
, pero los registros utilizados para contener los argumentos ahora casi coinciden con la convención de llamada de funciones:rdi
, rsi
, rdx
, r10
, r8
y r9
en ese orden. (syscall
mismo destruye rcx
entonces r10
se usa en lugar de rcx
, dejando que las funciones de envoltura de libc solo usen mov r10, rcx
/ syscall
.)
// x86-64 Linux
#include <asm/unistd.h> // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"syscall"
: "=a" (ret)
// EDI RSI RDX
: "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
: "rcx", "r11", "memory"
);
return ret;
}
(Véalo compilado en Godbolt)
Observe cómo prácticamente lo único que necesitaba cambiar eran los nombres de los registros y las instrucciones reales utilizadas para realizar la llamada. Esto se debe principalmente a las listas de entrada/salida proporcionadas por la sintaxis de ensamblado en línea extendida de gcc, que automáticamente proporciona las instrucciones de movimiento adecuadas necesarias para ejecutar la lista de instrucciones.
El "0"(callnum)
la restricción de coincidencia podría escribirse como "a"
porque el operando 0 (el "=a"(ret)
salida) solo tiene un registro para elegir; sabemos que elegirá EAX. Usa el que te resulte más claro.
Tenga en cuenta que los sistemas operativos que no son Linux, como MacOS, usan números de llamada diferentes. E incluso diferentes convenciones de paso de argumentos para 32 bits.