#c #linux
#c #linux
Вопрос:
Я рассматривал, как смещение конкретной переменной находится в данной структуре.
Я попробовал следующую программу.
struct info{
char a;
int b;
char c;
int d;
};
struct info myinfo;
int main(int argc, char **argv)
{
struct info *ptr = amp;myinfo;
unsigned int offset;
offset = (unsigned int) amp;((struct info *) 0)->d;
printf("Offset = %dn",offset);
return 0;
}
Я просто хотел знать, как offset = (unsigned int) amp;((struct info *) 0)->d
работает строка.
Я в замешательстве из-за разыменования 0.
Комментарии:
1. В случае, если это не очевидно: используйте
offsetof
вместо этого. Возможно, так оно и реализовано, но если это так, то знать, что это сработает, — проблема компилятора, а не ваша.2. @SteveJessop — Хотя я согласен, по какой-то причине разработчики ядра Linux этого не делают.
3. @Chris: ну, если есть разумная причина, тогда удачи им, но разработчики ядра Linux в прошлом допускали серьезные ошибки, когда они неправильно угадывали поведение GCC в ситуациях UB. Лучше не ввязываться во что-либо сложное, не зная причин 🙂
Ответ №1:
На самом деле это не разыменование 0
, хотя и выглядит так. Он действительно принимает адрес некоторого элемента, если он был разыменован по адресу 0
, гипотетически.
Это своего рода грязный взлом (плюс некоторые неприятные макросы), но он дает вам то, что вас интересует (смещение элемента в структуре).
Более «правильным» способом сделать то же самое было бы сгенерировать действительный объект, взять его адрес и взять адрес элемента, а затем вычесть их. Делать то же самое с нулевым указателем не совсем красиво, но работает без создания объекта и вычитания чего-либо.
Комментарии:
1. Просто собирался опубликовать то же самое. Может быть, другими словами: если бы эта структура начиналась с ячейки памяти 0, где был бы адрес члена
d
? Кстати: я не думаю, что это грязно 😉2. Стандарт C99 не требует
amp;((type *)0)->member
оптимизации разыменования. И даже если бы это было так, это оптимизировало бы to(char *)0 offsetof(type, member)
, который выполняет арифметику указателя наNULL
указатель, что все еще является неопределенным поведением. Это работает, потому что Linux компилируется только с помощью GCC, и GCC позволяет ему работать.3. @Chris Lutz: «выполняет арифметику указателей для нулевого указателя, что все еще является неопределенным поведением» — это точная причина, по которой я сказал «грязный». Но, к счастью, он все равно отлично работает на всех основных компиляторах (он тоже должен работать, если производитель компилятора реализует его таким образом).
4. Таким образом, это означает, что я могу дать что-либо, кроме 0. Например, ‘offset = (unsigned int) amp;((struct info *) 1000)-> d’ ??
5. @Damon — Достаточно справедливо. Также возможно использовать
1024
, который должен быть правильно выровнен для любого объекта и должен, по крайней мере, быть адресуемым. Технически разыменование может быть UB, но оно будет работать в том случае, если ваш компилятор предупреждает о выполнении арифметики указателей наNULL
. (Кто-то сказал мне здесь некоторое время назад, что им пришлось изменитьoffsetof
в своих системных заголовках с0
на1024
на, чтобы однажды избежать предупреждений от более нового компилятора.)
Ответ №2:
На самом деле вы не разыменовываете 0. Вы добавляете ноль и смещение элемента, поскольку вы берете адрес выражения. То есть, если off — это смещение элемента, вы делаете
0 off
не
*(0 off)
таким образом, вы никогда не выполняете доступ к памяти.
Комментарии:
1. Выполнение арифметики указателя для
NULL
указателя является неопределенным поведением. Это может сработать, и это работает для Linux, потому что они используют только GCC, но это не обязательно. Более надежным было бы использовать1024
, который должен быть выровнен с любой границей слов, используемой вашей системой, и должен быть адресуемым (хотя, вероятно, не разыменовываемым, но вам это не нужно).2. @Chris Lutz: Нет — это не выполнение незаконной арифметики указателей для нулевого указателя . NULL будет, потому что размер целевого значения NULL не определен (потому что это может быть
(int *0)
,(char *)0
,(void *)0
или что-то еще в зависимости от используемого компилятора). Но в этом примере компилятору совершенно ясно, что такое целевой тип, а 0 — это только начальное местоположение структуры, на которую указывается. Или сказал наоборот: если 0 не будет допустимым значением для указателя, списки смещений будут разбиты повсюду.3. @ktf — В стандарте явно указано, что арифметика указателей на нулевое значение указателя, независимо от типа, является неопределенным поведением. Вероятно, это связано с тем, что нулевой указатель не обязательно должен быть равен 0, и поэтому результирующее значение может не обязательно быть тем, что вы ожидаете. Вам разрешено выполнять арифметику указателей с целым числом 0:
int arr[4] = {0}; amp;arr[0]
но не с нулевым указателем:int *ptr = 0; amp;ptr[4];
4. @Chris Lutz: пожалуйста. укажите, где в стандарте это будет. Из того, что я вижу, не допускаются только выражения с константой нулевого указателя (это было бы
NULL
или(void *)0
), но, конечно, я могу использовать значения нулевого указателя , такие как(int *)0
или(struct foo *)0
5. @ktf — раздел, посвященный операторам сложения и вычитания (не запомнил номер, я ленив и на своем телефоне), определяет арифметику указателей только для объектов внутри одного массива (рассматривая объекты, не являющиеся массивом, как массив из 1), поэтому, поскольку нулевой указатель не указывает на объектнет допустимого «массива» для выполнения арифметики. И вы проигнорировали мою точку зрения о том, что нулевой указатель не обязательно должен быть равен нулю на уровне компьютера, что означает, что even
(char *)0 1
не обязательно даст вам указатель0x1
, а это означает, что результат должен быть неопределенным.