Как llvm узнает, указывает ли указатель на функцию-член на виртуальную функцию?

После написания кода, касающегося указателя на функцию-член, и чтения Itanium ABI # Указатели на функции-члены, я понял структуру указателя на функцию-член в llvm.

Но что меня поражает, так это то, как получить адрес функции с помощью указателя на функцию-член. Я не нашел способа определить, указывает ли указатель функции-члена mfptr на виртуальную функцию-член или на обычную функцию-член.


person JiaHao Xu    schedule 19.04.2018    source источник
comment
Не утверждаю, что это реализация LLVM, но, возможно, самая простая реализация - это то, что она не указывает на виртуальную функцию. Вместо этого компилятор может вставить безымянную, не виртуальную вспомогательную функцию, которая не имеет другой цели, кроме вызова виртуальной функции.   -  person MSalters    schedule 19.04.2018


Ответы (2)


В документации, которую вы связали, говорится:

Для виртуальной функции это 1 плюс смещение виртуальной таблицы (в байтах) функции.

В разделе Virtual Table Layout говорится:

смещения в виртуальной таблице определяются этой последовательностью распределения и естественным размером ABI и выравниванием

Смещение должно соответствовать требованиям выравнивания указателя функции.

Требования к выравниванию указателя функции, который является типом «POD», указаны в соответствующем C ABI. Я предполагаю, что указатели выровнены по размеру, поэтому адрес (и, следовательно, смещение) указателя должен быть четным числом, а его младший бит должен быть равен нулю.

Таким образом, реализация может просто проверить LSB поля смещения / указателя и знать, что тогда и только тогда, когда LSB является одним, она имеет дело с виртуальным методом.

Получив смещение в виртуальной таблице, он считывает указатель виртуальной таблицы из объекта и загружает фактический адрес функции из виртуальной таблицы, используя смещение от указателя элемента.

class C {
    virtual int someMethod();
};

int invokeAMethod(C *c, int (C::*method)()) {
    return (c->*method)();
}

На x86_64 clang действительно создает проверку LSB члена "ptr" указателя метода:

invokeAMethod(C*, int (C::*)()): # @invokeAMethod(C*, int (C::*)())
  // c is in rdi, method.adj is in rdx, and method.ptr is in rdx
  // adjust this pointer
  add rdi, rdx
  // check whether method is virtual
  test sil, 1
  // if it is not, skip the following
  je .LBB0_2
  // load the vtable pointer from the object
  mov rax, qword ptr [rdi]
  // index into the vtable with the corrected offset to load actual method address
  mov rsi, qword ptr [rax + rsi - 1]
.LBB0_2:
  // here the actual address of the method is in rsi, we call it
  // in this particular case we return the same type
  // and do not need to call any destructors
  // so we can tail call
  jmp rsi # TAILCALL

Я не смог поделиться ссылкой Godbolt для этого конкретного примера, потому что один из моих плагинов браузера вмешался, но вы можете сами поиграть с подобными примерами на https://gcc.godbolt.org.

person PaulR    schedule 19.04.2018

Я не уверен, что это именно то, что делает llvm, но я обнаружил, что разница между обычной функцией-членом и указателем виртуальной функции для Itanium ABI обычно заключается в структуре самой записи, как описано здесь:

Форма указателя виртуальной функции определяется специфичным для процессора C ++ ABI для реализации. В конкретном случае сборок 64-битных разделяемых библиотек Itanium запись указателя виртуальной функции содержит пару компонентов (каждый по 64 бита): значение целевого значения GP и фактический адрес функции. То есть, вместо того, чтобы быть обычным указателем функции, который указывает на такой двухкомпонентный дескриптор, дескриптором является запись указателя виртуальной функции.

Это нормальный указатель функции - это адрес, а указатель виртуальной функции - это дескриптор, созданный глобальным смещением позиции (GP) и эффективным адресом переопределения виртуальной функции. Теперь я предполагаю, что размер записи и какое-то украшение (если вы думаете, что в указанной вами ссылке упоминается цифра 1) позволяет отличить один тип указателя от указателя Другие.

РЕДАКТИРОВАТЬ

Я нашел еще один намек на определение записей vtable w.r.t. к виртуальным функциям-членам в реализации класса TargetCXXABI внешнего интерфейса clang для llvm. Этот класс предоставляет API (строка 181), который сообщает, функция-член выровнена или нет.

Это, как уже сказал PaulR в своем ответе, подтверждает тот факт, что LSB используется для различения нормальной функции-члена от виртуальной. Но это работает не из-за размера и выравнивания указателя - минимальная адресуемая единица всегда является байтом, поэтому указатели могут быть нечетными числами, - а потому, что в Itanium C ++ ABI тело обычных функций-членов выровнено так, что их адреса всегда четные. номера по дизайну.

Это не всегда так, и действительно, в реализации этого метода упоминается тот факт, что некоторые архитектуры (например, ARM) хранят дискриминатор в настройке указателя this, а не в адресе функции.

Таким образом, эта функция действительно привязана к архитектуре процессора, и, помимо общего правила LSB +1 для x86_64, вы должны проверить Itanium-подобный C ++ ABI каждого из них.

person Gabriella Giordano    schedule 19.04.2018
comment
Я думаю, что 1 используется, чтобы отличить nullptr от ptr, указывающего на виртуальную функцию, которая находится в первом элементе vtable. - person JiaHao Xu; 25.04.2018
comment
›Но потому что в Itanium C ++ ABI тело обычных функций-членов выровнено так, что их адреса всегда по дизайну являются четными числами. Где абзац про выравнивание тела? - person Cu2S; 21.11.2020