Як було сказано вище, правильна відповідь полягає в тому, щоб скласти все з VS2015, але для інтересу наступний мій аналіз проблеми.
Цей символ, схоже, не визначений в жодній статичній бібліотеці, наданій Microsoft як частина VS2015, що є досить своєрідним, оскільки всі інші є. Щоб дізнатись чому, нам потрібно подивитися декларацію цієї функції та, що ще важливіше, як вона використовується.
Ось фрагмент із заголовків Visual Studio 2008:
_CRTIMP FILE * __cdecl __iob_func(void);
#define stdin (&__iob_func()[0])
#define stdout (&__iob_func()[1])
#define stderr (&__iob_func()[2])
Отже, ми можемо бачити, що завдання функції полягає у поверненні початку масиву об'єктів FILE (а не ручками, "FILE *" - це ручка; FILE - основна непрозора структура даних, що зберігає важливі параметри стану). Користувачами цієї функції є три макроси stdin, stdout та stderr, які використовуються для різних викликів у стилі fscanf, fprintf.
Тепер давайте розглянемо, як Visual Studio 2015 визначає ті самі речі:
_ACRTIMP_ALT FILE* __cdecl __acrt_iob_func(unsigned);
#define stdin (__acrt_iob_func(0))
#define stdout (__acrt_iob_func(1))
#define stderr (__acrt_iob_func(2))
Таким чином, підхід змінився для функції заміни тепер повертати обробку файлу, а не адресу масиву файлових об'єктів, і макроси змінилися, щоб просто викликати функцію, що передає ідентифікаційний номер.
То чому вони не можуть / ми надаємо сумісний API? Є два ключові правила, яким Microsoft не може суперечити з точки зору їх первісної реалізації через __iob_func:
- Має бути масив із трьох структур FILE, які можна індексувати так само, як і раніше.
- Структурне розташування файлу FILE не може змінитися.
Будь-яка зміна будь-якого з перерахованих вище означатиме, що існуючий компільований код, пов'язаний проти цього, піде зовсім неправильно, якщо цей API викликається.
Давайте подивимось, як визначався / визначається FILE.
Спочатку визначення файлу VS2008:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
А тепер визначення VS2015 FILE:
typedef struct _iobuf
{
void* _Placeholder;
} FILE;
Отже, в цьому суть: структура змінила форму. Існуючий складений код, що посилається на __iob_func, покладається на те, що повернені дані є і масивом, який можна індексувати, і тим, що в цьому масиві елементи знаходяться на однаковій відстані.
Можливі рішення, згадані у відповідях вище у цьому напрямку, не спрацювали (якщо їх викликати) з кількох причин:
FILE _iob[] = {*stdin, *stdout, *stderr};
extern "C" FILE * __cdecl __iob_func(void)
{
return _iob;
}
Масив _iob масиву FILE був би складений з VS2015, і він буде викладений як блок структур, що містить порожнечу *. Якщо припустити 32-бітове вирівнювання, ці елементи будуть розташовані на 4 байти. Тож _iob [0] знаходиться в зсуві 0, _iob [1] - у зміщенні 4, а _iob [2] - у зміщенні 8. Код виклику замість цього очікує, що FILE буде значно довше, вирівнюється на 32 байти в моїй системі, і так він візьме адресу повернутого масиву і додасть 0 байт, щоб дістати до нуля елемента (цей нормальний), але для _iob [1] виведе, що йому потрібно додати 32 байти, а для _iob [2] він виведе що йому потрібно додати 64-байт (адже саме так воно виглядало в заголовках VS2008). І справді демонтований код для VS2008 демонструє це.
Вторинна проблема вищезазначеного рішення полягає в тому, що він копіює вміст структури FILE (* stdin), а не ручку FILE *. Отже, будь-який код VS2008 буде дивитись на іншу базову структуру, ніж VS2015. Це може спрацювати, якщо структура містить лише покажчики, але це великий ризик. У будь-якому випадку це питання не має жодного значення.
Єдиний хак, який мені вдалося приснити, - це той, в якому __iob_func проходить стек викликів, щоб визначити, яку фактичну обробку файлу вони шукають (на основі зміщення, доданої до повернутої адреси) і повертає обчислене значення таким, що воно дає правильну відповідь. Це кожен біт як божевільний, як це звучить, але прототип лише для x86 (не x64) перелічено нижче для вашого розваги. У моїх експериментах це працювало нормально, але ваш пробіг може відрізнятися - не рекомендується для використання у виробництві!
#include <windows.h>
#include <stdio.h>
#include <dbghelp.h>
/* #define LOG */
#if defined(_M_IX86)
#define GET_CURRENT_CONTEXT(c, contextFlags) \
do { \
c.ContextFlags = contextFlags; \
__asm call x \
__asm x: pop eax \
__asm mov c.Eip, eax \
__asm mov c.Ebp, ebp \
__asm mov c.Esp, esp \
} while(0);
#else
/* This should work for 64-bit apps, but doesn't */
#define GET_CURRENT_CONTEXT(c, contextFlags) \
do { \
c.ContextFlags = contextFlags; \
RtlCaptureContext(&c); \
} while(0);
#endif
FILE * __cdecl __iob_func(void)
{
CONTEXT c = { 0 };
STACKFRAME64 s = { 0 };
DWORD imageType;
HANDLE hThread = GetCurrentThread();
HANDLE hProcess = GetCurrentProcess();
GET_CURRENT_CONTEXT(c, CONTEXT_FULL);
#ifdef _M_IX86
imageType = IMAGE_FILE_MACHINE_I386;
s.AddrPC.Offset = c.Eip;
s.AddrPC.Mode = AddrModeFlat;
s.AddrFrame.Offset = c.Ebp;
s.AddrFrame.Mode = AddrModeFlat;
s.AddrStack.Offset = c.Esp;
s.AddrStack.Mode = AddrModeFlat;
#elif _M_X64
imageType = IMAGE_FILE_MACHINE_AMD64;
s.AddrPC.Offset = c.Rip;
s.AddrPC.Mode = AddrModeFlat;
s.AddrFrame.Offset = c.Rsp;
s.AddrFrame.Mode = AddrModeFlat;
s.AddrStack.Offset = c.Rsp;
s.AddrStack.Mode = AddrModeFlat;
#elif _M_IA64
imageType = IMAGE_FILE_MACHINE_IA64;
s.AddrPC.Offset = c.StIIP;
s.AddrPC.Mode = AddrModeFlat;
s.AddrFrame.Offset = c.IntSp;
s.AddrFrame.Mode = AddrModeFlat;
s.AddrBStore.Offset = c.RsBSP;
s.AddrBStore.Mode = AddrModeFlat;
s.AddrStack.Offset = c.IntSp;
s.AddrStack.Mode = AddrModeFlat;
#else
#error "Platform not supported!"
#endif
if (!StackWalk64(imageType, hProcess, hThread, &s, &c, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL))
{
#ifdef LOG
printf("Error: 0x%08X (Address: %p)\n", GetLastError(), (LPVOID)s.AddrPC.Offset);
#endif
return NULL;
}
if (s.AddrReturn.Offset == 0)
{
return NULL;
}
{
unsigned char const * assembly = (unsigned char const *)(s.AddrReturn.Offset);
#ifdef LOG
printf("Code bytes proceeding call to __iob_func: %p: %02X,%02X,%02X\n", assembly, *assembly, *(assembly + 1), *(assembly + 2));
#endif
if (*assembly == 0x83 && *(assembly + 1) == 0xC0 && (*(assembly + 2) == 0x20 || *(assembly + 2) == 0x40))
{
if (*(assembly + 2) == 32)
{
return (FILE*)((unsigned char *)stdout - 32);
}
if (*(assembly + 2) == 64)
{
return (FILE*)((unsigned char *)stderr - 64);
}
}
else
{
return stdin;
}
}
return NULL;
}