Различное поведение разыменования указателя - PullRequest
2 голосов
/ 01 ноября 2019

Я на самом деле пытался разобраться в макросе container_of() в Linux ядре, которое включает в себя что-то вроде этого:

#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif

#ifndef container_of
/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:    the pointer to the member.
 * @type:   the type of the container struct this is embedded in.
 * @member: the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({\
    const typeof(((type *)0)->member) * __mptr = (ptr);\
    (type *)((char *)__mptr - offsetof(type, member)); })

Я нашел выражение (type *)0)->member niggling. Я не мог понять, почему это выражение работает в этих макросах. Я прочитал эту статью, а затем попытался придумать программу, чтобы понять ее:

#include <stdio.h>

typedef struct {
    int first;
    int second;
    int third;
}group;

int main(){
    group a;
    printf("Address of second is %p, group is %p\n", &a.second, &a);
    size_t offset = &(((group*)0)->second);
    printf("Offset of second is %zd\n", offset );
    printf("Address of group is %p\n", (char*)&a.second - offset);
    int val = ((group*)0)->second;
}

Я понимаю, что выражение (group *)0)->member - это не указатель void*, аскорее указатель типа group, но в конечном итоге это не адрес NULL, на который ссылаются? Эта строка отлично работает

size_t offset = &(((group*)0)->second);

, тогда как это приводит к SIGSEGV

int val = ((group*)0)->second;

Чем отличается доступ к памяти в обоих случаях?

1 Ответ

2 голосов
/ 01 ноября 2019

Разница в том, что компилятор уже "знает" смещение во время компиляции и не нуждается в его вычислении, поэтому не требуется доступ к памяти и не возникает ошибка. Вот почему offsetof будет не работать с непрозрачной структурой . Это становится особенно ясным, когда вы проверяете соответствующий код сборки x86_64. Когда я запустил gcc -S для следующего кода C:

#include <stdio.h>

typedef struct {
    int first;
    int second;
    int third;
}group;

int main(){
    group a;
    size_t offset = (size_t) &(((group*)0)->second); # notice the cast to avoid a warning
    return 0;
}

было в основном только две инструкции, соответствующие содержанию моей программы на C:

movq    $4, -24(%rbp)       # move literal value 4 to *(rbp-24)
movl    $0, %eax            # move literal value 0 to eax (this is just a part of "return 0;" statement)

Если бы я сейчасизмените последние две строки в C на:

size_t offset = (size_t) &(((group*)0)->third);
return 1;

Код сборки будет отличаться только в этих двух инструкциях. Затем они будут читать:

movq    $8, -24(%rbp)   
movl    $1, %eax            

4 и 8 есть, потому что на моей машине int равно 4 байта. Что еще более важно, известно, каковы члены вашей структуры (именно поэтому непрозрачная структура не будет работать - эта информация скрыта.) Поскольку компилятор (или ассемблер) имеет эту информацию с самого начала, он может, и он просто делает«Жесткий код» это. Он не выполняет разыменования, потому что в этом нет необходимости.

Если я сейчас добавлю проблемную строку в мой код C:

#include <stdio.h>

typedef struct {
    int first;
    int second;
    int third;
}group;

int main(){
    group a;
    size_t offset = (size_t) &(((group*)0)->third);
    int val = ((group*)0)->second;
    return 0;
}

и соберу ее, я получу следующеедополнительные инструкции:

movl    $0, %eax            # move literal value 0 to eax
movl    4(%rax), %eax       # dereference the value at *(rax + 4) and save it in eax
movl    %eax, -28(%rbp)     # move the value saved at eax to the *(rbp - 28)

В первой строке просто хранится буквенное значение 0 в нижней половине регистра rax (верхняя половина в любом случае равна обнулена ). Segfault запускается в следующей инструкции, когда разыменовывается память в месте rax + 4 = 4 в попытке сохранить полученное значение в регистр eax. Фактически, здесь вы можете снова увидеть, что компилятор просто знает смещение struct group члена second по тому, как он просто смещает расположение структуры (сохраненной в rax) на буквальное значениеиз 4. Просто так получилось, что это недопустимая память, и, следовательно, ОС прерывает вашу программу, отправляя ее SIGSEGV.


Как сказано в комментариях, в первом примере вы 'не разыменовывать ничего, а только вычислять адрес. Во втором случае вы фактически разыменовываете указатель на 0, что приводит к segfault. Все это есть в статье , которую вы сами связали:

Теперь, когда смещение структуры «нормализовано», нам даже не важен размер зеленого элементаили размер структуры, потому что это легко, абсолютное смещение совпадает с относительным смещением. Это именно то, что делает & ((TYPE *) 0) -> MEMBER. Этот код разыменовывает структуру с нулевым смещением памяти.

Это обычно не очень умная вещь, но в этом случае этот код не выполняется или не обрабатывается. Это просто трюк, подобный тому, который я показал выше с рулеткой. Макрос offsetof () просто возвращает смещение элемента по сравнению с нулем. Это просто число, и вы не можете получить доступ к этой памяти . Поэтому при выполнении этого трюка вам нужно знать только тип конструкции.

(Мое выделение).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...