При отладке сторонней библиотеки столкнулся с такой проблемой. В деструкторе одного из классов есть инструкция:
if (cstr_)
valueAllocator()->releaseMemberName( const_cast<char *>(cstr_);
Во время работы программы при создании и уничтожении объектов (которые используют объекты отлаживаемого класса) всё функционирует нормально. Однако при завершении работы программы начинаются странные вещи. valueAllocator() возвращает указатель — *ValueAllocator. В отладчике видно, что __vfptr возвращённого указателя, состоящий из 5 элементов, содержит _purecall(void) с 1-й по 4-ю позиции. В итоге, получается, если я правильно понимаю, что-то вроде обращения по нулевому адресу. Получаю рантайм-ошибку R6025 (VS2008) с надписью "pure virtual function call".
Я пытался проверить, не нулевой ли адрес у ф-ции valueAllocator()->releaseMemberName, но ничего толкового из этого не вышло — всё время разные ошибки компиляции.
Подскажите, как проверить, что адрес действительно ненулевой (если это то, что здесь нужно), или может быть, есть какой-то др. способ решить проблему? Библиотека достаточно большая, и пытаться искать, почему такой указатель возвращается, — не самый подходящий вариант. Думается, проще проверить валидность указателя.
Re: Адрес функции-члена класса, или pure virtual call
у вас каким-то образом вызвался абстрактный метод. то есть определенный в какой-то декларации класса, но не имеющий тела.
если язык поощряет абстрактные методы, то разработчики компилятора разумеется генерируют таблицу виртуальных методов для такого класса, но в поле абстрактного метода запихивают адрес специальной функции, вся задача которой ругнуться по поводу ее вызова и сгенерировать ошибку с выходом из задачи.
вы попали именно на этот вариант.
вопросов два
1. каким образом к вам в руки попал обьект с нереализованным абстрактным методом.
2. почему вы вызвали его.
если код не ваш, то вопрос к разработчикам.
Re[2]: Адрес функции-члена класса, или pure virtual call
Здравствуйте, merk, Вы писали: M>1. каким образом к вам в руки попал обьект с нереализованным абстрактным методом. M>2. почему вы вызвали его. M>если код не ваш, то вопрос к разработчикам.
Код, действительно, не мой, и связаться с теми, кто его писал, мне не удастся.
Можно при вызове той функции как-то проверить, есть ли что вызывать?
Re: Адрес функции-члена класса, или pure virtual call
S>Код, действительно, не мой, и связаться с теми, кто его писал, мне не удастся. S>Можно при вызове той функции как-то проверить, есть ли что вызывать?
гм. вопрос сложный. еще неясно насколько хорошо работает ваш отладчик и где конкретно возник трэп.
диагностика говорит о том, что где-то в вашей строке идет переход управления на абстрактный метод.
если вы точно посмотрели таблицу VMT и там стоят адреса функции-заглушки,и вы точно установили, что выбирается один из этих заглушенных методов, то диагноз ясен.
у вас откуда-то возник обьект данный метод которого вы не имеете права вызывать.
значит нужно посмотреть
1. где этот обьект был создан? и какого актуального типа.
2. почему у обьекта данного типа в вашем коде вызывается нереализованный метод?
может этот код и не работал никогда? просто недоделка, ошибка и что-то еще?
посоветовать как его доделать?
откуда я знаю?
это зависит от той задачи что решается бибилиотекой и вашим конкретным этим вызовом.
Re[2]: Адрес функции-члена класса, или pure virtual call
S>Во время работы программы при создании и уничтожении объектов (которые используют объекты отлаживаемого класса) всё функционирует нормально. Однако при завершении работы программы начинаются странные вещи. valueAllocator() возвращает указатель — *ValueAllocator. В отладчике видно, что __vfptr возвращённого указателя, состоящий из 5 элементов, содержит _purecall(void) с 1-й по 4-ю позиции. В итоге, получается, если я правильно понимаю, что-то вроде обращения по нулевому адресу. Получаю рантайм-ошибку R6025 (VS2008) с надписью "pure virtual function call".
[кусь]
Если все начинается при завершении программы, то, скорее всего, проблема в том, что valueAllocator оказывается уже разрушенным в этот момент. Если проблемы возникают в деструкторах статических объектов, то причина в недетерминированном порядке создания/разрушения таких объектов, т.е. аллокатор уже умер, а тот кто его использует еще нет. Попробуйте отказаться от использования аллокатора в таких объектах, например, можно не освобождать память в таких случаях. Возможно, у аллокатора есть метод clone(), тогда можно использовать свою копию и управлять временем ее жизни. Ну и в самом крайнем случае можно попробовать вот такой грязный хак:
void* vfptr = *reinterpret_cast<void**>(valueAllocator());
if ( isValidVfptr(vfptr) ) // для проверки указателя можно запоминать его корректное значение.
valueAllocator()->releaseMemberName( const_cast<char *>(cstr_);
else// oops... allocator is dead now...
Смысл в том, что полиморфные объекты содержат указатель на таблицу виртуальных функций, после уничтожения объекта этот указатель изменится.
Re[2]: Адрес функции-члена класса, или pure virtual call
Здравствуйте, uzhas, Вы писали:
U>В конструкторе и деструкторе нельзя вызывать виртуальные функции. Это UB.
Нельзя вызывать чисто виртуальные функции. И это не зависит от того, конструктор, деструктор или кто угодно ещё их вызывает.
Можно делать статический вызов — при условии, что ЧВФ определена (если нет, будет ошибка линковки).
... << RSDN@Home 1.2.0 alpha rev. 655>>
Перекуём баги на фичи!
Re: Адрес функции-члена класса, или pure virtual call
Здравствуйте, Setver, Вы писали:
S>При отладке сторонней библиотеки столкнулся с такой проблемой. В деструкторе одного из классов есть инструкция:
Если что-то делаешь в деструкторе полиморфного класса — нужно исходить из того, что противоположное действие можно сделать в конструкторе этого же класса.
То есть, например, если отписываешься по имени класса, то и подписываешься по имени этого класса.
Вообще, в деструкторе время жизни объекта подходит к концу, и нужно тщательно следить за валидностью всех объектов вокруг.
Поскольку valueAllocator() не ЧВФ, то возможны 4 вещи:
— она возвращает указатель на объект, который был только что разрушен: в этом ли деструкторе, или в деструкторе наследника, или ещё где-то неподалёку
— на this, исходя из того, что объект — сам себе аллокатор;
— на сторонний объект в процессе разрушения (значит, мы ещё и нежданный реентер поймали)
— банальный расстрел памяти, наведённая ошибка
Посмотри по коду, где присваивается значение ValueAllocator, и почему там остаётся невалидный указатель. Где-то, наверно, его забыли или не успели обнулить.
S>Я пытался проверить, не нулевой ли адрес у ф-ции valueAllocator()->releaseMemberName, но ничего толкового из этого не вышло — всё время разные ошибки компиляции.
В рамках языка это невозможно. В С++ нет "адресов", есть указатели. Причём указатели на функции — это не совсем их адреса.
S>Подскажите, как проверить, что адрес действительно ненулевой (если это то, что здесь нужно), или может быть, есть какой-то др. способ решить проблему? Библиотека достаточно большая, и пытаться искать, почему такой указатель возвращается, — не самый подходящий вариант. Думается, проще проверить валидность указателя.
И даже если ты выцарапаешь фактический адрес функции (из содержимого vtbl), он будет ненулевым: там находится адрес функции-заглушки _purecall.
... << RSDN@Home 1.2.0 alpha rev. 655>>
Перекуём баги на фичи!
Re[2]: Адрес функции-члена класса, или pure virtual call
От:
Аноним
Дата:
11.06.08 11:04
Оценка:
К>В рамках языка это невозможно. В С++ нет "адресов", есть указатели. Причём указатели на функции — это не совсем их адреса.
указатель и адрес — что в лоб, что по лбу. другое дело что указатель на нечто, вовсе не обязан быть численно равен адресу, что вам кажется адресом обьекта. а немного смещен, например.
если указатель на функцию, "не совсем" ее адрес точки входа, то как вы предлагаете ее вызывать.
какой код вызова на асме будет у такого указателя на функцию?
Re[3]: Адрес функции-члена класса, или pure virtual call
Здравствуйте, <Аноним>, Вы писали:
А>указатель и адрес — что в лоб, что по лбу. другое дело что указатель на нечто, вовсе не обязан быть численно равен адресу, что вам кажется адресом обьекта. а немного смещен, например.
Он не "немного смещён", а немного по-другому устроен. Потому что должен в равной степени обслуживать и статические вызовы обычных методов (вот тут — да, фигурирует адрес кода), и виртуальные вызовы виртуальных методов.
А>если указатель на функцию, "не совсем" ее адрес точки входа, то как вы предлагаете ее вызывать. А>какой код вызова на асме будет у такого указателя на функцию?
struct Foo { virtual ~Foo() {} }; // покажем, что у Foo есть виртуальные методы в принципе
// для джигитов: можно вообще сделать предобъявление - тогда вызов будет предельно обобщённымstruct Foo;
// чтоб компилятор не вздумал инлайнитьextern Foo* object;
extern void (Foo::*method)();
int main()
{
(object->*method)();
}
Попроси компилятор родить асмовый листинг, да посмотри.
Вот что делает VC8
tempvar = -4 ; size = 4
_main PROC
; 7 : {
push ebp
mov ebp, esp
push ecx ; зарезервировали место для локальной переменной tempvar[ebp]
; 8 : (object->*method)();
cmp DWORD PTR ?method+12, 0 ; это указатель на виртуальную функцию?
jne SHORT $LN3@main
; нет, на обычную
mov eax, DWORD PTR ?object ; взяли адрес объекта
add eax, DWORD PTR ?method+4 ; прибавили смещение базы из указателя
mov DWORD PTR tempvar[ebp], eax ; запомнили адрес базы
jmp SHORT $LN4@main ; и пошли вызывать
$LN3@main: ; а вот указатель на виртуальную функцию...
mov ecx, DWORD PTR ?object ; берём указатель на объект
add ecx, DWORD PTR ?method+8 ; + смещение до vfptr = указатель на vfptr
mov edx, DWORD PTR ?object ; компилятор зачем-то продублировал... ну, его дело
add edx, DWORD PTR ?method+8 ; = указатель на vfptr
mov eax, DWORD PTR [edx] ; = vfptr
mov edx, DWORD PTR ?method+12 ; = индекс метода в таблице
add ecx, DWORD PTR [eax+edx] ; = смещение виртуальной базы (если есть) - первый элемент vtbl
add ecx, DWORD PTR ?method+4 ; + смещение базы
mov DWORD PTR tempvar[ebp], ecx ; = адрес искомой базы
$LN4@main:
mov ecx, DWORD PTR tempvar[ebp]
call DWORD PTR ?method ; вызвали метод (или диспетчер виртуальных методов)
; 9 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
... << RSDN@Home 1.2.0 alpha rev. 655>>
Перекуём баги на фичи!
Re[4]: Адрес функции-члена класса, или pure virtual call
вообще-то я имел ввиду обычный функциональный тип, а не тип — метод класса, тем более что виртуальная функция по сути является сама функциональным типом запихнутым в таблицу. оттого и все эти навороты.
К> cmp DWORD PTR ?method+12, 0 ; это указатель на виртуальную функцию? К> jne SHORT $LN3@main К> ; нет, на обычную К> mov eax, DWORD PTR ?object ; взяли адрес объекта К> add eax, DWORD PTR ?method+4 ; прибавили смещение базы из указателя К> mov DWORD PTR tempvar[ebp], eax ; запомнили адрес базы К> jmp SHORT $LN4@main ; и пошли вызывать
смотря на код..не понял что вы имеете ввиду под "базой". в данном случае в tempvar должен лежать адрес object, чтобы его протолкнуть как this? или это база от приколов связанных со реализацией множественного наследования?
К>$LN3@main: ; а вот указатель на виртуальную функцию...
К> mov ecx, DWORD PTR ?object ; берём указатель на объект К> add ecx, DWORD PTR ?method+8 ; + смещение до vfptr = указатель на vfptr К> mov edx, DWORD PTR ?object ; компилятор зачем-то продублировал... ну, его дело К> add edx, DWORD PTR ?method+8 ; = указатель на vfptr К> mov eax, DWORD PTR [edx] ; = vfptr К> mov edx, DWORD PTR ?method+12 ; = индекс метода в таблице К> add ecx, DWORD PTR [eax+edx] ; = смещение виртуальной базы (если есть) — первый элемент vtbl К> add ecx, DWORD PTR ?method+4 ; + смещение базы К> mov DWORD PTR tempvar[ebp], ecx ; = адрес искомой базы
К>$LN4@main: К> mov ecx, DWORD PTR tempvar[ebp] К> call DWORD PTR ?method ; вызвали метод (или диспетчер виртуальных методов)
тут что method указывал на точку входа в метод? не может быть. для виртуального метода это не сработало бы.
значит запустится еще какой-то диспетчер? гениально.
надо подумать таки как првильно реализовать вызов по переменной типа виртуальный метод.
Так врожде кажется что намудрено слишком.
Re[5]: Адрес функции-члена класса, или pure virtual call
Здравствуйте, merk, Вы писали:
M>вообще-то я имел ввиду обычный функциональный тип, а не тип — метод класса,
А топикстартер отчётливо имел в виду метод класса.
M> тем более что виртуальная функция по сути является сама функциональным типом запихнутым в таблицу. оттого и все эти навороты.
Давайте соблюдать чистоту языка.
Виртуальная функция не является функциональным типом, а тип в таблицу запихнуть — это я даже не представляю что может быть (если мы не говорим о смолтоке или питоне).
Поэтому такие аллегории нуждаются в развёрнутом пояснении.
Конечно, "нечто" засунуто в таблицу, и даже не в одну, а в множество таблиц (сколько у нас классов с виртуальными функциями есть).
Поскольку мы имеем дело с виртуальным вызовом, то приходится думать не о конкретной таблице и конкретной функции, а о совокупности таблиц. Про которые известно, что
— адрес функции находится по известному смещению во всех таблицах
— адрес таблицы находится по известному смещению в теле объекта
К>> mov eax, DWORD PTR ?object ; взяли адрес объекта К>> add eax, DWORD PTR ?method+4 ; прибавили смещение базы из указателя К>> mov DWORD PTR tempvar[ebp], eax ; запомнили адрес базы К>> jmp SHORT $LN4@main ; и пошли вызывать
M>смотря на код..не понял что вы имеете ввиду под "базой". в данном случае в tempvar должен лежать адрес object, чтобы его протолкнуть как this? или это база от приколов связанных со реализацией множественного наследования?
Нет, это даже в одиночном наследовании проявляется. Никто не обещает, что базовый подобъект находится с нулевым смещением в теле финального объекта.
Например, VC удобно помещать vfptr в нулевую позицию, поэтому
struct X
{
int x;
void foo();
};
struct Y : X
{
int y;
virtual void bar();
void buz();
};
struct Z : Y
{
int z;
void xyz();
};
ptrdiff_t delta(void const* a, void const* b) { return (char const*)a - (char const*)b; }
int main()
{
Z z;
ptrdiff_t dyz = delta(&z, &(Y&)z); // 0
ptrdiff_t dxz = delta(&z, &(X&)z); // 4
Y y;
ptrdiff_t dxy = delta(&y, &(X&)y); // 4void (Y::*method)();
method = &Y::buz; // method.base = 0
(z.*method)(); // сперва приводим Z& к Y&, это +0, затем из method +0, получаем корректный вызов ((Y&)z).buz()
method = &X::foo; // method.base = 4
(z.*method)(); // Z& -> Y& +0, из method берём Y& -> X& +4, получаем корректный вызов ((X&)(Y&)z).foo()
}
Для множественного наследования, разумеется, это тоже пригодится.
К>>$LN3@main: ; а вот указатель на виртуальную функцию...
Не поручусь, что правильно восстановил ход мысли компилятора — но там где-то примерно так. Если напутал, прошу прощения.
M>тут что method указывал на точку входа в метод? не может быть. для виртуального метода это не сработало бы. M>значит запустится еще какой-то диспетчер? гениально. M>надо подумать таки как првильно реализовать вызов по переменной типа виртуальный метод. M>Так врожде кажется что намудрено слишком.
Диспетчер там, как я понимаю, для "вообще". Чтобы последние строчки кода были одинаковы.
А то, что намудрено — ну так нужно же обеспечить правильную работу для всех возможных случаев.
Если про указатель на метод известно, что у класса нет виртуальных баз, или нет виртуальных методов, или вообще ничего нет — то и сам указатель уменьшается, и код его вызова упрощается.
... << RSDN@Home 1.2.0 alpha rev. 655>>
Перекуём баги на фичи!
Re[6]: Адрес функции-члена класса, или pure virtual call
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, merk, Вы писали:
M>>вообще-то я имел ввиду обычный функциональный тип, а не тип — метод класса,
К>А топикстартер отчётливо имел в виду метод класса.
M>> тем более что виртуальная функция по сути является сама функциональным типом запихнутым в таблицу. оттого и все эти навороты.
К>Давайте соблюдать чистоту языка.
Да ради бога! К>Виртуальная функция не является функциональным типом, а тип в таблицу запихнуть — это я даже не представляю что может быть (если мы не говорим о смолтоке или питоне).
имелось ввиду что VMT класса технически представляет собой массив адресов точек входа в тело виртуальных функций. а адрес точки входа в тело функции есть значение указателя на обычную функцию.
К>Поэтому такие аллегории нуждаются в развёрнутом пояснении.
пояснил.
К>Конечно, "нечто" засунуто в таблицу, и даже не в одну, а в множество таблиц (сколько у нас классов с виртуальными функциями есть). К>Поскольку мы имеем дело с виртуальным вызовом, то приходится думать не о конкретной таблице и конкретной функции, а о совокупности таблиц. Про которые известно, что К>- адрес функции находится по известному смещению во всех таблицах К>- адрес таблицы находится по известному смещению в теле объекта
неправда ваша. когда вы вызываете функцию-метод по указателю, вам думать не приходится о "всей совокупности" vmt в вашей программе. а только о той vmt, что можно взять у данного экземпляра класса. который вам подставят в качестве this. это одна таблица.
К>Нет, это даже в одиночном наследовании проявляется. Никто не обещает, что базовый подобъект находится с нулевым смещением в теле финального объекта. К>Например, VC удобно помещать vfptr в нулевую позицию, поэтому...
понято с vmt_ptr на нулевом смещении. до этого казалось, что они сделают -4. почему так не стоит в с++, понятно.
К>Диспетчер там, как я понимаю, для "вообще". Чтобы последние строчки кода были одинаковы. К>А то, что намудрено — ну так нужно же обеспечить правильную работу для всех возможных случаев.
Это странно. диспетчер не может работать быстро. может стоит какая-то дебагмода? надо попробовать без дебагмоды и с оптимизацией по скорости. должно дать чистый код.
К>Если про указатель на метод известно, что у класса нет виртуальных баз, или нет виртуальных методов, или вообще ничего нет — то и сам указатель уменьшается, и код его вызова упрощается.
очевидно. навороты там из-за виртуальности.
Re[6]: Адрес функции-члена класса, или pure virtual call
короче пришлось попробовать самому для С++ Visual Express 2005.
для чистоты картины отключены
— отладка
— RTTI
— все оптимизации
вердикт
указатель на метод хранится в формате
EntryPoint — адрес точки входа в метод[4 байта], Offset — смещение полей [4 байта]
метод вызывается так
— берется значение указателя на обьект с которым будет вызываться переменная-метод.
указатель корректируется на значение — Offset (см формат выше)
этот адрес считается как this для метода. он толкается в стек или регистр для передачи в метод.
делается call на EntryPoint.
это уже нравится, никаких порверок на виртуальный vs обычный не делается. и "диспетчеров" нет.
нестатический метод класса вызывается таким образом.
статический должен вызываться как обычная переменная-функция.
ваш могучий код порожден скорее всего отладкой плюс rtti.
Re[7]: Адрес функции-члена класса, или pure virtual call
Здравствуйте, merk, Вы писали:
M>ваш могучий код порожден скорее всего отладкой плюс rtti.
Нет, это ваш код порождён скорее всего отсутствием виртуального наследования.
Делайте вещи как можно проще, но не проще. Указатель на метод действительно можно уместить в sizeof(void(*)())+sizeof(ptrdiff_t), но для этого придётся на каждое использованное сочетание (класс,метод) заводить функцию-переходник.