c - propias - tipo de dato retorno
convención de llamadas x86: ¿deberían los argumentos pasados por la pila ser de solo lectura? (3)
El lenguaje de programación de C exige que los argumentos se pasen por valor . De modo que cualquier modificación de un argumento (como un x++;
como la primera instrucción de su foo
) es local para la función y no se propaga al llamador.
Por lo tanto, una convención de convocatoria general debería requerir la copia de argumentos en cada sitio de llamadas. Las convenciones de llamada deberían ser lo suficientemente generales para llamadas desconocidas , por ejemplo, a través de un puntero de función.
Por supuesto, si pasa una dirección a alguna zona de memoria, la función llamada puede desreferenciar ese puntero, por ejemplo, como en
int goo(int *x) {
static int count;
*x = count++;
return count % 3;
}
Por cierto, puede usar optimizaciones de tiempo de enlace (compilando y enlazando con clang -flto -O2
o gcc -flto -O2
) para quizás permitir al compilador mejorar o alinear algunas llamadas entre unidades de traducción.
Tenga en cuenta que tanto Clang / LLVM como GCC son compiladores de software libre . Siéntase libre de proponerles un parche de mejora si lo desea (pero dado que ambos son piezas de software muy complejas, tendrá que trabajar algunos meses para hacer ese parche).
NÓTESE BIEN. Al buscar el código ensamblado producido, pase -fverbose-asm
a su compilador.
Parece que los compiladores de última generación tratan los argumentos aprobados por la pila como de solo lectura. Tenga en cuenta que en la convención de llamadas x86, la persona que llama empuja los argumentos a la pila y el destinatario usa los argumentos en la pila. Por ejemplo, el siguiente código C:
extern int goo(int *x);
int foo(int x, int y) {
goo(&x);
return x;
}
se compila mediante clang -O3 -c gc -S -m32
en OS X 10.10 en:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 10
.globl _foo
.align 4, 0x90
_foo: ## @foo
## BB#0:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl 8(%ebp), %eax
movl %eax, -4(%ebp)
leal -4(%ebp), %eax
movl %eax, (%esp)
calll _goo
movl -4(%ebp), %eax
addl $8, %esp
popl %ebp
retl
.subsections_via_symbols
Aquí, el parámetro x
( 8(%ebp)
) se carga primero en %eax
; y luego se almacena en -4(%ebp)
; y la dirección -4(%ebp)
se almacena en %eax
; y %eax
se pasa a la función goo
.
Me pregunto por qué Clang genera código que copia el valor almacenado en 8(%ebp)
a -4(%ebp)
, en lugar de simplemente pasar la dirección 8(%ebp)
a la función goo
. Ahorrará las operaciones de memoria y dará como resultado un mejor rendimiento. Observé un comportamiento similar en GCC también (bajo OS X). Para ser más específico, me pregunto por qué los compiladores no generan:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 10
.globl _foo
.align 4, 0x90
_foo: ## @foo
## BB#0:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
leal 8(%ebp), %eax
movl %eax, (%esp)
calll _goo
movl 8(%ebp), %eax
addl $8, %esp
popl %ebp
retl
.subsections_via_symbols
Busqué documentos si la convención de llamadas x86 exige que los argumentos pasados sean de solo lectura, pero no pude encontrar nada sobre el tema. ¿Alguien tiene alguna idea sobre este tema?
En realidad, acabo de compilar esta función usando GCC:
int foo(int x)
{
goo(&x);
return x;
}
Y generó este código:
_foo:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
leal 8(%ebp), %eax
movl %eax, (%esp)
call _goo
movl 8(%ebp), %eax
leave
ret
Esto está usando GCC 4.9.2 (en cygwin de 32 bits si es importante), sin optimizaciones. De hecho, GCC hizo exactamente lo que pensaste que debería hacer y usó el argumento directamente desde donde lo colocó la persona que llamó en la pila.
Las reglas para C son que los parámetros deben pasarse por valor. Un compilador convierte de un idioma (con un conjunto de reglas) a un idioma diferente (potencialmente con un conjunto de reglas completamente diferente). La única limitación es que el comportamiento sigue siendo el mismo. Las reglas del lenguaje C no se aplican al idioma de destino (por ejemplo, ensamblaje).
Lo que esto significa es que si un compilador tiene ganas de generar lenguaje ensamblador donde los parámetros se pasan por referencia y no se pasan por valor; entonces esto es perfectamente legal (mientras el comportamiento permanezca igual).
La verdadera limitación no tiene nada que ver con C en absoluto. La verdadera limitación es vincular. Para que los diferentes archivos de objeto se puedan vincular entre sí, se necesitan estándares para garantizar que todo lo que la persona que llama en un archivo de objeto espera coincide con lo que proporciona el destinatario en otro archivo de objeto. Esto es lo que se conoce como ABI. En algunos casos (por ejemplo, de 64 bits, 80x86), existen múltiples ABI diferentes para la misma arquitectura.
Incluso puedes inventar tu propio ABI que sea radicalmente diferente (e implementar tus propias herramientas que soporten tu propio ABI radicalmente diferente) y eso es perfectamente legal en lo que respecta a los estándares C; incluso si su ABI requiere "pasar por referencia" para todo (siempre que el comportamiento siga siendo el mismo).