Mengapa alamat argc dan argv 12 byte terpisah?

40

Saya menjalankan program berikut di komputer saya (Intel 64-bit menjalankan 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;
}

Output dari program ini adalah

$ 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

Ukuran pointer &argvadalah 8 byte. Saya mengharapkan alamat argcmenjadi address of (argv) + sizeof (argv) = 0x7ffed1a4c9f0 + 0x8 = 0x7ffed1a4c9f8tetapi ada padding 4 byte di antara mereka. Mengapa demikian?

Dugaan saya adalah bahwa hal itu bisa disebabkan oleh penyelarasan memori, tetapi saya tidak yakin.

Saya melihat perilaku yang sama dengan fungsi yang saya panggil juga.

letmutx
sumber
15
Kenapa tidak? Mereka bisa terpisah 174 byte. Jawaban akan tergantung pada sistem operasi Anda dan / atau perpustakaan pembungkus yang melakukan setup untuk main.
aschepler
2
@aschepler: Seharusnya tidak tergantung pada pembungkus yang melakukan setup main. Dalam C, maindapat disebut sebagai fungsi reguler, sehingga perlu menerima argumen seperti fungsi biasa dan harus mematuhi ABI.
Eric Postpischil
@aschelper: Saya perhatikan perilaku yang sama untuk fungsi lain juga.
letmutx
4
Ini adalah 'eksperimen pikiran' yang menarik, tapi sungguh, tidak ada yang lebih dari 'Aku ingin tahu mengapa'. Alamat-alamat ini dapat berubah tergantung pada os, kompiler, versi kompiler, arsitektur prosesor dan sama sekali tidak harus bergantung pada 'kehidupan nyata'.
Neil
2
hasil sizeof harus dicetak menggunakan%zu
phuclv

Jawaban:

61

Pada sistem Anda, beberapa argumen integer atau pointer pertama dilewatkan dalam register dan tidak memiliki alamat. Ketika Anda mengambil alamatnya dengan &argcatau &argv, kompiler harus membuat alamat dengan menulis konten register ke lokasi stack dan memberi Anda alamat lokasi stack tersebut. Dalam melakukan hal itu, kompiler memilih, dalam arti tertentu, lokasi tumpukan apa pun yang cocok untuknya.

Eric Postpischil
sumber
6
Perhatikan bahwa ini bisa terjadi bahkan jika mereka dilewatkan di tumpukan ; kompiler tidak memiliki kewajiban untuk menggunakan slot nilai-masuk pada tumpukan sebagai penyimpanan untuk objek lokal nilai-nilai itu masuk. Mungkin masuk akal untuk melakukan ini karena fungsi ini pada akhirnya akan memanggil-ekor dan membutuhkan nilai saat ini dari objek-objek ini untuk menghasilkan argumen keluar untuk panggilan-ekor.
R .. GitHub BERHENTI MEMBANTU ICE
10

Mengapa alamat argc dan argv 12 byte terpisah?

Dari perspektif standar bahasa, jawabannya adalah "tidak ada alasan khusus". C tidak menentukan atau menyiratkan hubungan apa pun antara alamat parameter fungsi. @EricPostpischil menjelaskan apa yang mungkin terjadi dalam implementasi khusus Anda, tetapi detail-detail itu akan berbeda untuk implementasi di mana semua argumen dilewatkan di stack, dan itu bukan satu-satunya alternatif.

Terlebih lagi, saya mengalami masalah dengan cara di mana informasi tersebut dapat berguna dalam suatu program. Misalnya, bahkan jika Anda "tahu" bahwa alamatnya argvadalah 12 byte sebelum alamat argc, masih belum ada cara yang pasti untuk menghitung salah satu dari pointer tersebut dari yang lain.

John Bollinger
sumber
7
@ R..GitHubSTOPHELPINGICE: Menghitung satu dari yang lain sebagian didefinisikan, tidak didefinisikan dengan baik. Standar C tidak ketat pada bagaimana konversi untuk uintptr_tdilakukan, dan itu tentu tidak mendefinisikan hubungan antara alamat parameter atau di mana argumen dilewatkan.
Eric Postpischil
6
@ R..GitHubSTOPHELPINGICE: Fakta bahwa Anda dapat pulang pergi berarti bahwa g (f (x)) = x, di mana x adalah sebuah pointer, f adalah convert-pointer-to-uintptr_t, dan g adalah convert-uintptr_t-to -poiner. Secara matematis dan logis, itu tidak menyiratkan bahwa g (f (x) +4) = x + 4. Misalnya, jika f (x) adalah x² dan g (y) adalah sqrt (y), maka g (f (x)) = x (untuk x non-negatif nyata), tetapi g (f (x) +4) ≠ x + 4, secara umum. Dalam kasus pointer, konversi ke uintptr_tmungkin memberikan alamat dalam 24 bit tinggi dan beberapa bit otentikasi dalam 8 bit rendah. Kemudian menambahkan 4 hanya mengacaukan otentikasi; itu tidak memperbarui ...
Eric Postpischil
5
... bit alamat. Atau konversi ke uintptr_t mungkin memberikan alamat dasar di 16 bit tinggi dan offset di bit 16 rendah, dan menambahkan 4 bit rendah mungkin membawa ke bit tinggi, tetapi penskalaannya salah (karena alamat yang diwakili tidak basis • 65536 + offset tetapi basis • 64 + offset, seperti pada beberapa sistem). Sederhananya, yang uintptr_tAnda dapatkan dari konversi belum tentu alamat yang sederhana.
Eric Postpischil
4
@ R..GitHubSTOPHELPINGICE dari pembacaan standar saya, hanya ada jaminan lemah yang (void *)(uintptr_t)(void *)pakan dibandingkan dengan (void *)p. Dan perlu dicatat bahwa panitia telah mengomentari hampir masalah yang tepat ini, menyimpulkan bahwa "implementasi ... juga dapat memperlakukan pointer berdasarkan asal yang berbeda meskipun mereka sedikit identik ."
Ryan Avella
5
@ R..GitHubSTOPHELPINGICE: Maaf, saya melewatkan bahwa Anda menambahkan nilai yang dihitung sebagai perbedaan dari dua uintptr_tkonversi alamat daripada perbedaan pointer atau jarak "diketahui" dalam byte. Tentu, itu benar, tetapi bagaimana ini berguna? Tetap benar bahwa "masih belum ada cara yang pasti untuk menghitung salah satu petunjuk dari yang lain" sebagai jawaban menyatakan, tetapi perhitungan itu tidak menghitung bdari amelainkan menghitung bdari keduanya adan b, karena bharus digunakan dalam pengurangan untuk menghitung jumlah menambahkan. Menghitung satu dari yang lain tidak ditentukan.
Eric Postpischil