tipos - ¿Por qué el compilador de VC++ de 64 bits agrega instrucción nop luego de las llamadas a funciones?
parametros formales en c++ (2)
He compilado lo siguiente con Visual Studio C ++ 2008 SP1, compilador x64
C++
:
Tengo curiosidad, ¿por qué el compilador agregó esas instrucciones de nop
luego de esas call
?
PS1. Yo entiendo que el 2do y 3er nop
s serían alinear el código en un margen de 4 bytes, pero el 1er nop
rompe esa suposición.
PS2. El código de C ++ que se compiló no tenía bucles ni elementos especiales de optimización:
CTestDlg::CTestDlg(CWnd* pParent /*=NULL*/)
: CDialog(CTestDlg::IDD, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
//This makes no sense. I used it to set a debugger breakpoint
::GdiFlush();
srand(::GetTickCount());
}
PS3. Información adicional: En primer lugar, gracias a todos por su aporte.
Aquí hay observaciones adicionales:
Mi primera suposición fue que el enlace incremental podría haber tenido algo que ver con eso. Sin embargo, la configuración de compilación
Release
enVisual Studio
para el proyecto tiene unincremental linking
desactivado.Esto parece afectar únicamente a las compilaciones
x64
. El mismo código construido comox86
(oWin32
) no tiene esosnop
s, aunque las instrucciones utilizadas son muy similares:
- Traté de compilarlo con un enlazador más nuevo, y aunque el código
x64
producido porVS 2013
ve algo diferente, todavía agrega esosnop
s después de algunascall
s:
- También
static
vinculacióndynamic
vsstatic
a MFC no hizo ninguna diferencia en la presencia de esosnop
s. Este está creado con enlaces dinámicos a dlls de MFC conVS 2013
:
- También tenga en cuenta que esos
nop
s pueden aparecer después de lascall
near
y defar
también, y no tienen nada que ver con la alineación. Aquí hay una parte del código que obtuve deIDA
si avanzo un poco más:
Como puede ver, el nop
se inserta después de una call
far
que sucede para "alinear" la siguiente instrucción lea
en la dirección B
Eso no tiene sentido si se agregaron solo para la alineación.
- Originalmente me inclinaba a creer que, dado que las
call
relative
near
(es decir, aquellas que comienzan conE8
) son algo más rápidas que lascall
far
(o las que comienzan conFF
,15
en este caso)
el enlazador puede tratar de acercarse primero a las call
near
, y dado que son call
un byte más cortas que far
, si tiene éxito, puede rellenar el espacio restante con nop
s al final. Pero luego el ejemplo (5) de arriba derrota esta hipótesis.
Entonces todavía no tengo una respuesta clara a esto.
Esto es solo una suposición, pero podría ser algún tipo de optimización SEH. Digo optimización porque SEH parece funcionar bien sin los NOP también. NOP podría ayudar a acelerar el desenrollamiento.
En el siguiente ejemplo ( demostración en vivo con VC2017 ), hay un NOP
insertado después de una llamada a basic_string::assign
en test1
pero no en test2
(idéntico pero declarado como non-throwing 1 ).
#include <stdio.h>
#include <string>
int test1() {
std::string s = "a"; // NOP insterted here
s += getchar();
return (int)s.length();
}
int test2() throw() {
std::string s = "a";
s += getchar();
return (int)s.length();
}
int main()
{
return test1() + test2();
}
Montaje:
test1:
. . .
call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
npad 1 ; nop
call getchar
. . .
test2:
. . .
call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
call getchar
Tenga en cuenta que MSVS compila de forma predeterminada con el /EHsc
(manejo de excepciones sincrónicas). Sin esa bandera, los NOP
desaparecerán, y con /EHa
(manejo de excepciones sincrónico y asíncrono), throw()
ya no hace la diferencia porque SEH siempre está activo.
1 Por alguna razón, solo throw()
parece reducir el tamaño del código, utilizando noexcept
hace que el código generado sea aún más grande y convoca aún más NOP
s. MSVC ...
Esto se debe a una convención de llamadas en x64 que requiere que la pila esté alineada con 16 bytes antes de cualquier instrucción de llamada. Esto no es (para mi conocimiento) un requisito de hardware sino uno de software. Esto proporciona una manera de asegurarse de que al ingresar una función (es decir, después de una instrucción de llamada), el valor del puntero de la pila sea siempre 8 módulo 16. De este modo, permite la alineación simple de datos y el almacenamiento / lectura de la ubicación alineada en la pila.