Указатель функции всегда равен нулю, но работает при разыменовании и вызове

Вот минимальный пример кода, который у меня есть (я попробовал запустить минимальный пример, чтобы убедиться, что он воспроизводит проблему, которую я вижу):

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. Я что-то пропустил?

Что произойдет, если вы используете printf("fp: %p\n", (void *) &testfn);? Преобразование %p предназначено для приема void *, а не любого другого типа указателя.

Eric Postpischil 20.04.2024 02:09

@EricPostpischil Действительно ли указатели на функции конвертируются в void *?

Some programmer dude 20.04.2024 02:20

@Someprogrammerdude: Да. Стандарт C не определяет преобразование, но не мешает вам его делать.

Eric Postpischil 20.04.2024 02:29

Обратите внимание, что testfn и &testfn одинаковы по значению и типу.

Erik Eidt 20.04.2024 02:35

Хорошая мысль. Я попробовал выполнить приведение к (void *) без каких-либо изменений в выводе или поведении.

tedtanner 20.04.2024 02:56

@tedtanner, попробуйте printf("fp: %jX\n", (uintmax_t) &testfn);, так как void * может быть слишком узким для указателя функции. Кроме того, давайте найдем размер указателя функции с помощью printf("%zu\n", sizeof(&testfn));

chux - Reinstate Monica 20.04.2024 08:49

Это может быть связано с тем, что вы создаете какую-то встроенную систему. Если вы собираете для своего ПК, получите ли вы тот же результат?

Some programmer dude 20.04.2024 12:31

Если вы производите сборку для какой-либо встроенной системы, проверка файла карты может показать то, чего вам следует ожидать.

Gerhardh 20.04.2024 15:54

RISC-V, очевидно, может защищать исполняемые страницы от чтения. Может, вот что здесь происходит?

tofro 23.04.2024 11:17
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
9
138
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Похоже, что система буквально помещает раздел кода по адресу 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]);. Я подозреваю, что это не все нули.

dbush 20.04.2024 03:35

Связано: stackoverflow.com/questions/77321435/…

dbush 20.04.2024 03:41

@dbush распечатывает память по нулевому адресу и дает ненулевые значения

tedtanner 20.04.2024 04:21

Не память по нулевому адресу, а представление NULL-указателя.

dbush 20.04.2024 04:30

Нулевой указатель выводится как 0x0000000000000000, когда я печатаю его с помощью %p. Реализация printf в системе является базовой и не поддерживает %02x.

tedtanner 20.04.2024 04:45

Тедтаннер, %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-битным, что объясняет ваши проблемы.

chux - Reinstate Monica 20.04.2024 09:03

«Нулевая страница» (первый раздел памяти) обычно используется для разных целей. Некоторые небольшие встроенные системы нередко начинают выполнение по этому адресу. Или хранить таблицу прерываний или другие системно-зависимые данные по адресу 0.

Some programmer dude 20.04.2024 12:31

Также обратите внимание, что NULL не обязательно должно быть равно 0. Исторически существовали системы, где это имело место.

Some programmer dude 20.04.2024 12:33

Какова будет ценность, если вы объявите указатель на функцию и напечатаете его необработанное значение, например, void (*fnPtr)(void) = testfn; unsigned char rawPtrValue[sizeof(fnPtr)]; memcpy(rawPtrValue, &fnPtr, sizeof(rawPtrValue));? Это позволит избежать любых возможных преобразований адреса функции в void *, и вы сможете увидеть размер указателя функции.

Andrew Henle 20.04.2024 14:28

> Никогда ничего не должно быть по адресу 0x0. Почему часть адресного пространства (хотя бы один байт) должна быть потрачена впустую только потому, что в некоторых языках 0 используется как специальное значение для указателя? Не все архитектуры поддерживают виртуализацию адресов.

dimich 20.04.2024 20:16

@chux-ReinstateMonica Отличные мысли. Однако регистр void * по-прежнему показывает ноль. Указатель не усекается; указатель функции не шире void * в этой системе. Я обнаружил проблему в системе (это было довольно сложное решение), из-за которой раздел кода отображался на виртуальный адрес 0x0 в таблице страниц процесса, так что у меня есть достаточно, чтобы подтвердить этот вывод. Однако вы правы в том, что мой ответ, опубликованный здесь, не обязательно доказывает то, что я пытаюсь доказать.

tedtanner 21.04.2024 05:05

@Someprogrammerdude Приятно это знать. Обычно я избегаю сопоставления этой первой страницы, потому что многие программисты полагаются на то, что она не отображается, но вы правы в том, что это не обязательно должно быть так. Я все еще считаю, что отображать эту страницу — плохая идея. А еще желаю удачи в объяснении немцам, что null не обязательно должен быть 0 xD

tedtanner 21.04.2024 05:08

@dimich Только ситхи имеют дело с абсолютами, и, видимо, я ситх. Без виртуализации адресов я, скорее всего, отказался бы от своего утверждения. Мое мнение таково, что в системах с виртуализацией адресов, поскольку многие программисты предполагают, что 0 не отображается, не следует отображать эту страницу, чтобы избежать проблем, возникающих из-за неверных предположений.

tedtanner 21.04.2024 05:14

@tedtanner Сопоставлять или не сопоставлять - это выбор ОС, а не оборудования и не языка.

dimich 21.04.2024 06:11

@tedtanner Пожалуйста, рассмотрите возможность добавления результата printf("%u %d %p\n", (unsigned) sizeof &testfn2, &testfn2 == NULL, (void *) NULL);.

chux - Reinstate Monica 21.04.2024 18:51

Другие вопросы по теме