clang++ stdcall bug
Наконец-то собрав чудопроект clang
’ом я отважился его запустить и
тут-же получил SIGSEGV
. Отлов самого бага занял у меня почти сутки.
Я знал, что ищу, но я не мог гарантированно воспроизвести вылет.
Рассмотрим такой пример:
interface.hpp
:
//====---- interface.hpp ----=====//
#ifndef __interface_hpp__
#define __interface_hpp__
#define FN_ARGS int a, int b, int c, int d, int e, int f
#define FN_ARGV 1, 2, 3, 4, 5, 6
extern "C" void foo (void);
class I {
public:
virtual void Startup(FN_ARGS) __attribute__((stdcall));
};
#endif // __interface_hpp__
interface.cc
:
//====---- interface.cc ----=====//
#include "interface.hh"
void I::Startup(FN_ARGS) //STDCALL
{
();
foo}
//====---- main.cc ----=====//
#include <stdio.h>
#include "interface.hh"
void foo (void) { }
int main (int argc, char * * argv)
{
;
I _d.Startup(FN_ARGV);
_dreturn 0;
}
Итак, мы тут видим:
I::Startup()
-stdcall
функция с6
параматрамиfoo()
-cdecl
функция с0
параметрами
Запускаем всё это таким злым скриптом (генерим 32-битный код,
так как на amd64
stdcall
ничего не значит и там всё работает):
#!/bin/sh
export LANG=C
rm -rf *.o
CXX=clang++; OEXT=o_clang
#CXX=g++; OEXT=o_gcc
CXXFLAGS="-m32 -g -O2 -fomit-frame-pointer"
for f in *.cc
do
$CXX $CXXFLAGS -c "$f" -o "$f.$OEXT"
done
$CXX $CXXFLAGS *.$OEXT -o prog
./prog
$ ./mk.sh
./mk.sh: line 18: 5606 Segmentation fault ./prog
Ух, свалилось. Посмотрим во что собралась наша I::Startup()
:
g++
:
$ objdump -d -S interface.cc.o_gcc
00000000 <_ZN1I7StartupEiiiiii>:
//====---- interface.cc ----=====//
#include "interface.hh"
void I::Startup(FN_ARGS) //STDCALL
0: 83 ec 0c sub $0xc,%esp
{
3: e8 fc ff ff ff call 4 <_ZN1I7StartupEiiiiii+0x4>
foo();
8: 83 c4 0c add $0xc,%esp
b: c2 1c 00 ret $0x1c
g++
правильно реализует stdcall
конвенцию: вызывает функцию foo()
и сам чистит за собой стек:
ret $0x1c
- выталкивает со стека 7
параметров (6
явных + this
)
clang++
:
objdump -d -S interface.cc.o_clang
00000000 <_ZN1I7StartupEiiiiii>:
//====---- interface.cc ----=====//
#include "interface.hh"
void I::Startup(FN_ARGS) //STDCALL
{
foo();
0: e9 fc ff ff ff jmp 1 <_ZN1I7StartupEiiiiii+0x1>
Oшибка: I::Startup()
не трогает стек, будто мы в cdecl
функции. Естественно, это ведет
к тому, что после возврата из функции foo()
мы вернемся в main()
, но у него будет сдвинутый
на 28
байт стек. Код возврата из main()
свалится и мы получим наш SIGSEGV
.