Вот минимальный пример кода, который у меня есть (я попробовал запустить минимальный пример, чтобы убедиться, что он воспроизводит проблему, которую я вижу):
void testfn(void) {
printf("Hello, world!\n");
}
int main(int argc, char *argv[]) {
printf("fp: %p\n", &testfn);
return 0;
}
В результате я получаю fp: 0x0000000000000000
, что указывает на то, что указатель функции, который я пытаюсь напечатать, равен 0. Я попробовал использовать атрибут GCC __attribute__((noinline))
для testfn()
и получил тот же результат.
Я компилирую для системы RISC-V и работаю в QEMU. Я подумал, что, возможно, реализация printf()
была ошибочной (хотя мне она выглядит хорошо, и другие указатели печатаются нормально), поэтому я попробовал привести указатель функции к беззнаковому 64-битному int, а затем зациклить такое количество итераций (я понимаю, что это технически UB, но на практике обычно работает и это тупой тест), но цикл выполняет ровно 0 итераций:
void __attribute__((noinline)) testfn(void) {
printf("Hello, world!\n");
}
int main(int argc, char *argv[]) {
void (*fn)(void) = &testfn;
printf("Testing printf...\n");
printf("fp addr: %p\n", &fn);
fn();
for (int i = 0; i < (unsigned long long) fn; ++i) {
printf("Here!\n");
}
printf("fp: %p\n", fn);
return 0;
}
Это дает следующий результат:
Testing printf...
fp addr: 0x0000000000003FA8
Hello, world!
fp: 0x0000000000000000
Вот команда, используемая для компиляции: riscv64-unknown-elf-gcc -Wall -O0 -fno-omit-frame-pointer -ggdb -gdwarf-2 -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie -c -o user/rkttest.o user/rkttest.c
Вот команда, которую я использую для запуска QEMU; qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
.
Мне кажется, что указатель функции всегда равен нулю, за исключением случаев, когда я пытаюсь вызвать его как функцию. Мне кажется, это ошибка компилятора или какая-то странная вещь, происходящая в моей среде, но я решил рассказать об этом здесь, на Stack Overflow. Я что-то пропустил?
@EricPostpischil Действительно ли указатели на функции конвертируются в void *
?
@Someprogrammerdude: Да. Стандарт C не определяет преобразование, но не мешает вам его делать.
Обратите внимание, что testfn
и &testfn
одинаковы по значению и типу.
Хорошая мысль. Я попробовал выполнить приведение к (void *)
без каких-либо изменений в выводе или поведении.
@tedtanner, попробуйте printf("fp: %jX\n", (uintmax_t) &testfn);
, так как void *
может быть слишком узким для указателя функции. Кроме того, давайте найдем размер указателя функции с помощью printf("%zu\n", sizeof(&testfn));
Это может быть связано с тем, что вы создаете какую-то встроенную систему. Если вы собираете для своего ПК, получите ли вы тот же результат?
Если вы производите сборку для какой-либо встроенной системы, проверка файла карты может показать то, чего вам следует ожидать.
RISC-V, очевидно, может защищать исполняемые страницы от чтения. Может, вот что здесь происходит?
Похоже, что система буквально помещает раздел кода по адресу 0x0, а компилятор помещает функцию в начало раздела кода. Это ДЕЙСТВИТЕЛЬНО плохая идея со стороны системы. По адресу 0x0 никогда не должно быть ничего.
Следующий код:
static void __attribute__((noinline)) testfn(void) {
printf("Hello, world!\n");
}
static void __attribute__((noinline)) testfn2(void) {
printf("Hello, world!\n");
}
int main(int argc, char *argv[]) {
printf("fp: %p\n", (void *) &testfn);
printf("fp: %p\n", (void *) &testfn2);
return 0;
}
выдает следующий результат:
fp: 0x000000000000001A
fp: 0x0000000000000000
О, парень. 🤦♂️
Редактировать: Хорошо, возможно, утверждение «ничего не должно быть по адресу 0x0, никогда» не обязательно верно (противоположность: системы без виртуальной адресации). Но я по-прежнему считаю, что для систем с виртуальной адресацией отображать эту страницу — очень плохая идея, хотя бы во избежание проблем, возникающих из-за неверных предположений.
Любопытно, что это напечатает: void *p = NULL; unsigned char *c = (unsigned char *)&p; for (int i;i<sizeof(void*); i++) printf("%02x ", c[i]);
. Я подозреваю, что это не все нули.
Связано: stackoverflow.com/questions/77321435/…
@dbush распечатывает память по нулевому адресу и дает ненулевые значения
Не память по нулевому адресу, а представление NULL-указателя.
Нулевой указатель выводится как 0x0000000000000000
, когда я печатаю его с помощью %p. Реализация printf в системе является базовой и не поддерживает %02x.
Тедтаннер, %p
ожидает void *
. &testfn
не является void *
, поэтому printf("fp: %p\n", &testfn);
имеет неопределенное поведение (UB). Таким образом, выводы этого ответа не имеют достаточного подтверждения. printf("fp: %p\n", (void *) &testfn);
лучше, но это может привести к усечению указателя функции (который может быть шире, чем void *
). Сначала давайте найдем размер указателя функции с помощью printf("%zu\n", sizeof(&testfn));
или printf("%u\n", (unsigned) sizeof(&testfn));
. AFAIK, указатель функции здесь может быть 128-битным, что объясняет ваши проблемы.
«Нулевая страница» (первый раздел памяти) обычно используется для разных целей. Некоторые небольшие встроенные системы нередко начинают выполнение по этому адресу. Или хранить таблицу прерываний или другие системно-зависимые данные по адресу 0
.
Также обратите внимание, что NULL
не обязательно должно быть равно 0
. Исторически существовали системы, где это имело место.
Какова будет ценность, если вы объявите указатель на функцию и напечатаете его необработанное значение, например, void (*fnPtr)(void) = testfn; unsigned char rawPtrValue[sizeof(fnPtr)]; memcpy(rawPtrValue, &fnPtr, sizeof(rawPtrValue));
? Это позволит избежать любых возможных преобразований адреса функции в void *
, и вы сможете увидеть размер указателя функции.
> Никогда ничего не должно быть по адресу 0x0. Почему часть адресного пространства (хотя бы один байт) должна быть потрачена впустую только потому, что в некоторых языках 0
используется как специальное значение для указателя? Не все архитектуры поддерживают виртуализацию адресов.
@chux-ReinstateMonica Отличные мысли. Однако регистр void *
по-прежнему показывает ноль. Указатель не усекается; указатель функции не шире void *
в этой системе. Я обнаружил проблему в системе (это было довольно сложное решение), из-за которой раздел кода отображался на виртуальный адрес 0x0 в таблице страниц процесса, так что у меня есть достаточно, чтобы подтвердить этот вывод. Однако вы правы в том, что мой ответ, опубликованный здесь, не обязательно доказывает то, что я пытаюсь доказать.
@Someprogrammerdude Приятно это знать. Обычно я избегаю сопоставления этой первой страницы, потому что многие программисты полагаются на то, что она не отображается, но вы правы в том, что это не обязательно должно быть так. Я все еще считаю, что отображать эту страницу — плохая идея. А еще желаю удачи в объяснении немцам, что null не обязательно должен быть 0 xD
@dimich Только ситхи имеют дело с абсолютами, и, видимо, я ситх. Без виртуализации адресов я, скорее всего, отказался бы от своего утверждения. Мое мнение таково, что в системах с виртуализацией адресов, поскольку многие программисты предполагают, что 0 не отображается, не следует отображать эту страницу, чтобы избежать проблем, возникающих из-за неверных предположений.
@tedtanner Сопоставлять или не сопоставлять - это выбор ОС, а не оборудования и не языка.
@tedtanner Пожалуйста, рассмотрите возможность добавления результата printf("%u %d %p\n", (unsigned) sizeof &testfn2, &testfn2 == NULL, (void *) NULL);
.
Что произойдет, если вы используете
printf("fp: %p\n", (void *) &testfn);
? Преобразование%p
предназначено для приемаvoid *
, а не любого другого типа указателя.