Почему адреса argc и argv разделены 12 байтами?

40

Я запустил на своем компьютере следующую программу (64-разрядная версия Intel под управлением Linux).

#include <stdio.h>

void test(int argc, char **argv) {
    printf("[test] Argc Pointer: %p\n", &argc);
    printf("[test] Argv Pointer: %p\n", &argv);
}

int main(int argc, char **argv) {
    printf("Argc Pointer: %p\n", &argc);
    printf("Argv Pointer: %p\n", &argv);
    printf("Size of &argc: %lu\n", sizeof (&argc));
    printf("Size of &argv: %lu\n", sizeof (&argv));
    test(argc, argv);
    return 0;
}

Выход программы был

$ gcc size.c -o size
$ ./size
Argc Pointer: 0x7fffd7000e4c
Argv Pointer: 0x7fffd7000e40
Size of &argc: 8
Size of &argv: 8
[test] Argc Pointer: 0x7fffd7000e2c
[test] Argv Pointer: 0x7fffd7000e20

Размер указателя &argvсоставляет 8 байт. Я ожидал, что адрес argcбудет, address of (argv) + sizeof (argv) = 0x7ffed1a4c9f0 + 0x8 = 0x7ffed1a4c9f8но между ними есть 4-байтовые отступы. Почему это так?

Я предполагаю, что это может быть связано с выравниванием памяти, но я не уверен.

Я замечаю такое же поведение и с функциями, которые я вызываю.

letmutx
источник
15
Почему бы нет? Они могут быть на расстоянии 174 байта. Ответ будет зависеть от вашей операционной системы и / или библиотеки-оболочки, для которой выполняется настройка main.
Ашеплер
2
@aschepler: это не должно зависеть от какой-либо оболочки, которая настроена для main. В C mainможет вызываться как обычная функция, поэтому она должна получать аргументы как обычная функция и должна подчиняться ABI.
Эрик Постпишил
@aschelper: я замечаю такое же поведение и для других функций.
letmutx
4
Это интересный «мысленный эксперимент», но на самом деле нет ничего, что должно быть больше, чем «я удивляюсь, почему». Эти адреса могут меняться в зависимости от операционной системы, компилятора, версии компилятора, архитектуры процессора и никоим образом не должны зависеть в «реальной жизни».
Нил

Ответы:

61

В вашей системе первые несколько целочисленных или указательных аргументов передаются в регистрах и не имеют адресов. Когда вы берете их адреса с помощью &argcили &argv, компилятор должен сфабриковать адреса, записав содержимое регистра в ячейки стека и предоставив вам адреса этих расположений стека. При этом компилятор выбирает, в каком-то смысле, те места расположения стека, которые ему подходят.

Эрик Постпищил
источник
6
Обратите внимание, что это может произойти, даже если они передаются в стеке ; компилятор не обязан использовать слот входящего значения в стеке в качестве хранилища для локальных объектов, в которые попадают значения. Возможно, имеет смысл сделать это, если функция, в конечном счете, будет выполнять хвостовой вызов и нуждается в текущих значениях этих объектов для создания исходящих аргументов для хвостового вызова.
R .. GitHub ОСТАНОВИТЬ ЛЬДА
10

Почему адреса argc и argv разделены 12 байтами?

С точки зрения языкового стандарта ответом является «без особой причины». C не указывает и не подразумевает какую-либо связь между адресами параметров функции. @EricPostpischil описывает, что, вероятно, происходит в вашей конкретной реализации, но эти детали будут другими для реализации, в которой все аргументы передаются в стек, и это не единственная альтернатива.

Более того, мне сложно найти способ, которым такая информация может быть полезна в программе. Например, даже если вы «знаете», что адрес argvсоставляет 12 байтов перед адресом argc, все равно не существует определенного способа вычисления одного из этих указателей из другого.

Джон Боллинджер
источник
7
@ R..GitHubSTOPHELPINGICE: Вычисление одного из другого частично определено, а не четко определено. Стандарт C не является строгим в отношении того, как выполняется преобразование uintptr_t, и он, безусловно, не определяет отношения между адресами параметров или местом передачи аргументов.
Эрик Постпишил
6
@ R..GitHubSTOPHELPINGICE: тот факт, что вы можете использовать циклический переход, означает, что g (f (x)) = x, где x - указатель, f - convert-pointer-pointer-to-uintptr_t, а g - convert-uintptr_t-to -указатель. Математически и логически это не означает, что g (f (x) +4) = x + 4. Например, если f (x) были x², а g (y) были sqrt (y), то g (f (x)) = x (для реального неотрицательного x), но g (f (x) +4) ≠ х + 4, в общем. В случае указателей преобразование в uintptr_tможет дать адрес в старших 24 битах и ​​некоторые биты аутентификации в младших 8 битах. Затем добавление 4 просто облажает аутентификацию; это не обновляет…
Эрик Постпишил
5
... адресные биты. Или преобразование в uintptr_t может дать базовый адрес в старших 16 битах и ​​смещение в младших 16 битах, а добавление 4 к младшим битам может привести к старшим битам, но масштабирование неверно (поскольку представленный адрес не является base • 65536 + смещение, а скорее base • 64 + смещение, как это было в некоторых системах). Проще говоря, uintptr_tвы получаете от преобразования не обязательно простой адрес.
Эрик Постпишил
4
@ R..GitHubSTOPHELPINGICE из моего прочтения стандарта, есть только слабая гарантия, которая (void *)(uintptr_t)(void *)pбудет равна (void *)p. И стоит отметить, что комитет прокомментировал практически эту проблему, заключив, что «реализации ... могут также рассматривать указатели, основанные на различном происхождении, как отличающиеся, даже если они поразрядно идентичны ».
Райан Авелла
5
@ R..GitHubSTOPHELPINGICE: Извините, я пропустил, что вы добавляли значение, рассчитанное как разность двух uintptr_tпреобразований адресов, а не разность указателей или «известное» расстояние в байтах. Конечно, это правда, но чем это полезно? Это остается верным , что «есть все еще не определен способ вычислить одну из этих указателей от другого» как ответ государств, но расчет не рассчитывается bот aа вычисляет bот обоих aи b, поскольку bдолжны быть использованы при вычитании для вычисления суммы добавить. Вычисление одного из другого не определено.
Эрик Постпишил