Разница в том, что компилятор уже "знает" смещение во время компиляции и не нуждается в его вычислении, поэтому не требуется доступ к памяти и не возникает ошибка. Вот почему 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 () просто возвращает смещение элемента по сравнению с нулем. Это просто число, и вы не можете получить доступ к этой памяти . Поэтому при выполнении этого трюка вам нужно знать только тип конструкции.
(Мое выделение).