Dlaczego adresy argc i argv 12 bajtów są oddzielone?

40

Na swoim komputerze uruchomiłem następujący program (64-bitowy Intel z systemem 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;
}

Dane wyjściowe programu były

$ 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

Rozmiar wskaźnika &argvwynosi 8 bajtów. Spodziewałem adres argc, aby być address of (argv) + sizeof (argv) = 0x7ffed1a4c9f0 + 0x8 = 0x7ffed1a4c9f8, ale nie jest wyściółka 4 bajt pomiędzy nimi. Dlaczego tak jest?

Domyślam się, że może to wynikać z wyrównania pamięci, ale nie jestem pewien.

Zauważam to samo zachowanie w przypadku wywoływanych funkcji.

letmutx
źródło
15
Dlaczego nie? Mogą być w odległości 174 bajtów. Odpowiedź będzie zależeć od systemu operacyjnego i / lub biblioteki opakowania, która konfiguruje main.
aschepler
2
@aschepler: Nie powinno to zależeć od żadnego opakowania, które się skonfiguruje main. W C mainmożna wywoływać jako funkcję zwykłą, dlatego musi odbierać argumenty jak funkcja zwykła i musi być zgodny z ABI.
Eric Postpischil
@aschelper: Zauważam to samo zachowanie dla innych funkcji.
letmutx
4
To interesujący „eksperyment myślowy”, ale tak naprawdę nie ma nic, co powinno być więcej niż „zastanawiam się, dlaczego”. Adresy te mogą się zmieniać w zależności od systemu operacyjnego, kompilatora, wersji kompilatora, architektury procesora i w żadnym wypadku nie powinny być zależne od „prawdziwego życia”.
Neil

Odpowiedzi:

61

W twoim systemie pierwsze argumenty liczb całkowitych lub wskaźników są przekazywane do rejestrów i nie mają adresów. Kiedy bierzesz ich adresy za pomocą &argclub &argv, kompilator musi sfabrykować adresy, pisząc zawartość rejestru do lokalizacji stosu i podając adresy tych lokalizacji stosu. W ten sposób kompilator wybiera w pewnym sensie dogodne dla niego lokalizacje stosu.

Eric Postpischil
źródło
6
Zauważ, że może się to zdarzyć, nawet jeśli zostaną przekazane na stos ; kompilator nie ma obowiązku korzystania ze szczeliny wartości przychodzących na stosie jako pamięci dla obiektów lokalnych, do których przechodzą wartości. Może to mieć sens, ponieważ funkcja będzie ostatecznie wywoływać ogon i potrzebuje bieżących wartości tych obiektów do wygenerowania argumentów wychodzących dla wywołania ogona.
R .. GitHub ZATRZYMAJ LÓD
10

Dlaczego adresy argc i argv 12 bajtów są oddzielone?

Z punktu widzenia standardu językowego odpowiedź brzmi „bez konkretnego powodu”. C nie określa ani nie implikuje żadnego związku między adresami parametrów funkcji. @EricPostpischil opisuje to, co prawdopodobnie dzieje się w twojej konkretnej implementacji, ale te szczegóły byłyby inne dla implementacji, w której wszystkie argumenty są przekazywane na stos, i to nie jest jedyna alternatywa.

Co więcej, mam problem z wymyśleniem sposobu, w jaki takie informacje mogą być przydatne w programie. Na przykład, nawet jeśli „wiesz”, że adres argvwynosi 12 bajtów przed adresem argc, nadal nie ma określonego sposobu obliczenia jednego z tych wskaźników z drugiego.

John Bollinger
źródło
7
@ R..GitHubSTOPHELPINGICE: Obliczanie jednego z drugiego jest częściowo zdefiniowane, a nie dobrze zdefiniowane. Standard C nie jest ścisły w kwestii sposobu konwersji na uintptr_t, a na pewno nie definiuje relacji między adresami parametrów lub w których przekazywane są argumenty.
Eric Postpischil
6
@ R..GitHubSTOPHELPINGICE: Fakt, że możesz przejść w obie strony oznacza, że ​​g (f (x)) = x, gdzie x jest wskaźnikiem, f oznacza konwersję wskaźnika na uintptr_t, a g oznacza konwersję-uintptr_t-to -wskaźnik. Matematycznie i logicznie nie oznacza to, że g (f (x) +4) = x + 4. Na przykład, jeśli f (x) wynosi x², a g (y) to sqrt (y), to g (f (x)) = x (dla rzeczywistego nieujemnego x), ale g (f (x) +4) ≠ x + 4 ogólnie. W przypadku wskaźników konwersja na uintptr_tmoże dać adres w wysokich 24 bitach i niektóre bity uwierzytelnienia w niskich 8 bitach. Następnie dodanie 4 po prostu psuje uwierzytelnianie; nie aktualizuje…
Eric Postpischil
5
… Bity adresu. Lub konwersja na uintptr_t może dać adres bazowy w wysokich 16 bitach i przesunięcie w niskich 16 bitach, a dodanie 4 do niskich bitów może prowadzić do wysokich bitów, ale skalowanie jest nieprawidłowe (ponieważ reprezentowany adres nie jest przesunięcie podstawy • 65536 +, ale raczej przesunięcie podstawy • 64 +, jak to miało miejsce w niektórych systemach). Mówiąc wprost, uintptr_tkonwersja nie musi być prostym adresem.
Eric Postpischil
4
@ R..GitHubSTOPHELPINGICE z mojego czytania standardu, jest tylko słaba gwarancja, która (void *)(uintptr_t)(void *)pbędzie porównywana równa (void *)p. Warto zauważyć, że komitet skomentował prawie ten właśnie problem, stwierdzając, że „implementacje… mogą również traktować wskaźniki oparte na różnych źródłach jako odrębne, nawet jeśli są bitowo identyczne ”.
Ryan Avella
5
@ R..GitHubSTOPHELPINGICE: Przepraszam, tęskniłem za dodaniem wartości obliczonej jako różnica dwóch uintptr_tkonwersji adresów zamiast różnych wskaźników lub „znanej” odległości w bajtach. Jasne, to prawda, ale jak to jest przydatne? Pozostaje prawdą, że „nadal nie ma określonego sposobu obliczenia jednego z tych wskaźników od drugiego”, jak stwierdza odpowiedź, ale obliczenia te nie obliczają bz, alecz obliczają bz obu, ai bponieważ bnależy je zastosować do odejmowania w celu obliczenia kwoty dodać. Obliczanie jednego od drugiego nie jest zdefiniowane.
Eric Postpischil