Mengapa fungsi pointer dan pointer data tidak kompatibel dalam C / C ++?

130

Saya telah membaca bahwa mengonversi fungsi pointer ke data pointer dan sebaliknya berfungsi pada sebagian besar platform tetapi tidak dijamin berfungsi. Mengapa demikian? Bukankah keduanya seharusnya hanya alamat ke memori utama dan karenanya kompatibel?

gexicide
sumber
16
Tidak terdefinisi dalam standar C, didefinisikan dalam POSIX. Pikirkan perbedaannya.
ephemient
Saya sedikit baru dalam hal ini, tetapi bukankah Anda seharusnya melakukan pemeran di sisi kanan "="? Menurut saya masalahnya adalah Anda menetapkan penunjuk kosong. Tetapi saya melihat bahwa halaman manual melakukan ini, jadi semoga seseorang dapat mendidik saya. Saya melihat contoh di 'internet orang yang memasukkan nilai kembali dari dlsym, misalnya di sini: daniweb.com/forums/thread62561.html
JasonWoof
9
Perhatikan apa yang POSIX katakan di bagian Tipe Data : §2.12.3 Tipe Pointer. Semua tipe penunjuk fungsi harus memiliki representasi yang sama dengan penunjuk tipe void. Konversi dari pointer fungsi ke void *tidak akan mengubah representasi. Sebuah void *nilai yang dihasilkan dari konversi tersebut dapat dikonversi kembali ke jenis pointer fungsi asli, menggunakan cast yang eksplisit, tanpa kehilangan informasi. Catatan : Standar ISO C tidak memerlukan ini, tetapi diperlukan untuk kesesuaian POSIX.
Jonathan Leffler
2
ini adalah pertanyaan di bagian TENTANG situs web ini .. :) :) Sampai jumpa di sini
ZooZ
1
@KeithThompson: dunia berubah - dan POSIX juga. Apa yang saya tulis pada 2012 tidak lagi berlaku pada 2018. Standar POSIX mengubah kata-kata. Sekarang dikaitkan dengan dlsym()- perhatikan bagian akhir 'Penggunaan Aplikasi' di mana dikatakan: Perhatikan bahwa konversi dari void *pointer ke pointer fungsi seperti pada: fptr = (int (*)(int))dlsym(handle, "my_function"); tidak ditentukan oleh standar ISO C. Standar ini mengharuskan konversi ini bekerja dengan benar pada penyesuaian implementasi.
Jonathan Leffler

Jawaban:

171

Arsitektur tidak harus menyimpan kode dan data dalam memori yang sama. Dengan arsitektur Harvard, kode dan data disimpan dalam memori yang sama sekali berbeda. Sebagian besar arsitektur adalah arsitektur Von Neumann dengan kode dan data dalam memori yang sama tetapi C tidak membatasi dirinya hanya untuk jenis arsitektur tertentu jika memungkinkan.

Dirk Holsopple
sumber
15
Juga, bahkan jika kode dan data disimpan di tempat yang sama dalam perangkat keras fisik, perangkat lunak dan akses memori sering mencegah berjalannya data sebagai kode tanpa "persetujuan" sistem operasi. DEP dan sejenisnya.
Michael Graczyk
15
Setidaknya sama pentingnya dengan memiliki ruang alamat yang berbeda (mungkin lebih penting) adalah bahwa pointer fungsi mungkin memiliki representasi yang berbeda dari pointer data.
Michael Burr
14
Anda bahkan tidak harus memiliki arsitektur Harvard untuk memiliki pointer kode dan data menggunakan ruang alamat yang berbeda - model memori DOS "Small" yang lama melakukan ini (near pointers with CS != DS).
caf
1
bahkan prosesor modern akan berjuang dengan campuran seperti instruksi dan cache data biasanya ditangani secara terpisah, bahkan ketika sistem operasi memungkinkan Anda untuk menulis kode di suatu tempat.
PypeBros
3
@EricJ. Sampai Anda menelepon VirtualProtect, yang memungkinkan Anda untuk menandai wilayah data sebagai yang dapat dieksekusi.
Dietrich Epp
37

Beberapa komputer memiliki (memiliki) ruang alamat yang terpisah untuk kode dan data. Pada perangkat keras seperti itu tidak berfungsi.

Bahasa dirancang tidak hanya untuk aplikasi desktop saat ini, tetapi untuk memungkinkannya diimplementasikan pada perangkat keras yang besar.


Sepertinya komite bahasa C tidak pernah dimaksudkan void*untuk berfungsi sebagai penunjuk, mereka hanya ingin penunjuk generik ke objek.

The C99 Rationale mengatakan:

6.3.2.3 Petunjuk
C sekarang telah diterapkan pada berbagai arsitektur. Sementara beberapa dari arsitektur ini menampilkan pointer seragam yang berukuran sebesar beberapa tipe integer, kode portable maksimal tidak dapat mengasumsikan korespondensi yang diperlukan antara tipe pointer yang berbeda dan tipe integer. Pada beberapa implementasi, pointer bahkan bisa lebih luas dari tipe integer apa pun.

Penggunaan void*("pointer to void") sebagai tipe pointer objek generik adalah penemuan Komite C89. Adopsi tipe ini dirangsang oleh keinginan untuk menentukan argumen prototipe fungsi yang secara diam-diam mengkonversi pointer arbitrer (seperti pada fread) atau mengeluh jika tipe argumen tidak sama persis (seperti pada strcmp). Tidak ada yang dikatakan tentang pointer ke fungsi, yang mungkin tidak sepadan dengan pointer objek dan / atau bilangan bulat.

Catatan Tidak ada yang dikatakan tentang pointer ke fungsi di paragraf terakhir. Mereka mungkin berbeda dari petunjuk lain, dan panitia sadar akan hal itu.

Bo Persson
sumber
Standar dapat membuat mereka kompatibel tanpa mengacaukan ini dengan hanya membuat tipe data ukuran yang sama dan menjamin bahwa menetapkan satu dan kemudian kembali akan menghasilkan nilai yang sama. Mereka melakukan ini dengan void *, yang merupakan satu-satunya jenis pointer yang kompatibel dengan semuanya.
Edward Strange
15
@CrazyEddie Anda tidak dapat menetapkan fungsi pointer ke a void *.
ouah
4
Saya bisa salah pada void * menerima pointer fungsi, tetapi intinya tetap. Bit adalah bit. Standar dapat mensyaratkan bahwa ukuran dari berbagai jenis dapat mengakomodasi data dari satu sama lain dan tugas akan dijamin untuk bekerja bahkan jika mereka digunakan dalam segmen memori yang berbeda. Alasan ketidakcocokan ini ada adalah bahwa ini TIDAK dijamin oleh standar sehingga data dapat hilang dalam penugasan.
Edward Strange
5
Tetapi membutuhkan sizeof(void*) == sizeof( void(*)() )ruang akan terbuang dalam kasus di mana pointer fungsi dan pointer data ukuran yang berbeda. Ini adalah kasus umum di tahun 80-an, ketika standar C pertama ditulis.
Robᵩ
8
@RichardChambers: Ruang alamat yang berbeda mungkin juga memiliki lebar alamat yang berbeda , seperti Atmel AVR yang menggunakan 16 bit untuk instruksi dan 8 bit untuk data; dalam hal ini, akan sulit mengkonversi dari data (8 bit) untuk berfungsi (16 bit) pointer dan kembali lagi. C seharusnya mudah diimplementasikan; bagian dari kemudahan itu berasal dari meninggalkan data dan petunjuk instruksi yang tidak kompatibel satu sama lain.
John Bode
30

Bagi mereka yang ingat MS-DOS, Windows 3.1 dan yang lebih lama jawabannya cukup mudah. Semua ini digunakan untuk mendukung beberapa model memori yang berbeda, dengan beragam kombinasi karakteristik untuk penunjuk kode dan data.

Jadi misalnya untuk model Compact (kode kecil, data besar):

sizeof(void *) > sizeof(void(*)())

dan sebaliknya dalam model Medium (kode besar, data kecil):

sizeof(void *) < sizeof(void(*)())

Dalam hal ini Anda tidak memiliki penyimpanan terpisah untuk kode dan tanggal tetapi masih tidak dapat mengkonversi antara dua petunjuk (singkat menggunakan pengubah __near dan __far jauh non-standar).

Selain itu tidak ada jaminan bahwa meskipun pointer memiliki ukuran yang sama, bahwa mereka menunjuk ke hal yang sama - dalam model memori DOS Kecil, baik kode dan data yang digunakan dekat pointer, tetapi mereka menunjuk ke segmen yang berbeda. Jadi mengkonversi pointer fungsi ke data pointer tidak akan memberi Anda pointer yang memiliki hubungan dengan fungsi sama sekali, dan karenanya tidak ada gunanya untuk konversi seperti itu.

Tomek
sumber
Re: "mengkonversi pointer fungsi ke data pointer tidak akan memberi Anda pointer yang memiliki hubungan dengan fungsi sama sekali, dan karenanya tidak ada gunanya untuk konversi seperti itu": Ini tidak sepenuhnya mengikuti. Mengonversikan int*ke void*memberi Anda sebuah pointer yang tidak dapat Anda lakukan apa-apa, tetapi tetap bermanfaat untuk dapat melakukan konversi. (Hal ini karena void*dapat menyimpan setiap objek pointer, sehingga dapat digunakan untuk algoritma generik yang tidak perlu tahu apa jenis yang mereka pegang Hal yang sama dapat berguna untuk fungsi pointer juga, jika diizinkan..)
ruakh
4
@ruakh: Dalam hal mengkonversi int *ke void *, void *dijamin untuk setidaknya menunjuk ke objek yang sama seperti aslinya int *- jadi ini berguna untuk algoritma umum yang mengakses objek menunjuk-ke, seperti int n; memcpy(&n, src, sizeof n);. Dalam kasus di mana mengkonversi pointer fungsi ke void *tidak menghasilkan pointer menunjuk pada fungsi, itu tidak berguna untuk algoritma seperti itu - satu-satunya hal yang bisa Anda lakukan adalah mengonversi void *kembali ke pointer fungsi lagi, jadi Anda mungkin bisa baik hanya menggunakan yang unionberisi void *dan fungsi pointer.
caf
@caf: Cukup adil. Terima kasih telah menunjukkannya. Dan dalam hal ini, bahkan jika void* memang menunjuk ke fungsi, saya kira itu akan menjadi ide yang buruk bagi orang untuk meneruskannya memcpy. :-P
ruakh
Disalin dari atas: Perhatikan apa yang POSIX katakan dalam Tipe Data : §2.12.3 Jenis Pointer. Semua tipe penunjuk fungsi harus memiliki representasi yang sama dengan penunjuk tipe void. Konversi dari pointer fungsi ke void *tidak akan mengubah representasi. Sebuah void *nilai yang dihasilkan dari konversi tersebut dapat dikonversi kembali ke jenis pointer fungsi asli, menggunakan cast yang eksplisit, tanpa kehilangan informasi. Catatan : Standar ISO C tidak memerlukan ini, tetapi diperlukan untuk kesesuaian POSIX.
Jonathan Leffler
@caf Jika itu hanya harus diteruskan ke beberapa panggilan balik yang tahu jenis yang tepat, saya hanya tertarik pada keselamatan pulang pergi, bukan hubungan lain yang mungkin memiliki nilai yang dikonversi.
Deduplicator
23

Pointer untuk membatalkan seharusnya mampu mengakomodasi pointer ke semua jenis data - tetapi tidak harus pointer ke suatu fungsi. Beberapa sistem memiliki persyaratan berbeda untuk pointer ke fungsi daripada pointer ke data (misalnya, ada DSP dengan pengalamatan berbeda untuk data vs kode, model medium pada MS-DOS menggunakan pointer 32-bit untuk kode tetapi hanya pointer 16-bit untuk data) .

Jerry Coffin
sumber
1
tetapi kemudian seharusnya fungsi dlsym () tidak mengembalikan sesuatu selain dari kekosongan *. Maksud saya, jika void * tidak cukup besar untuk fungsi pointer, bukankah kita sudah fubared?
Manav
1
@Knickerkicker: Ya, mungkin. Jika memori berfungsi, tipe pengembalian dari dlsym telah dibahas panjang lebar, mungkin 9 atau 10 tahun yang lalu, pada daftar email OpenGroup. Begitu saja, saya tidak ingat apa (jika ada) yang terjadi.
Jerry Coffin
1
kamu benar. Ini sepertinya ringkasan yang cukup bagus (walaupun ketinggalan zaman) dari poin Anda.
Manav
2
@LegoStormtroopr: Menarik bagaimana 21 orang setuju dengan gagasan pemungutan suara, tetapi hanya sekitar 3 yang benar-benar melakukannya. :-)
Jerry Coffin
13

Selain apa yang sudah dikatakan di sini, menarik untuk melihat POSIX dlsym():

Standar ISO C tidak mengharuskan pointer ke fungsi dapat dilemparkan bolak-balik ke pointer ke data. Memang, standar ISO C tidak mengharuskan objek bertipe void * dapat menahan pointer ke suatu fungsi. Implementasi yang mendukung ekstensi XSI, bagaimanapun, mengharuskan objek tipe void * dapat menahan pointer ke suatu fungsi. Namun, hasil konversi pointer ke fungsi menjadi pointer ke tipe data lain (kecuali void *) masih belum ditentukan. Perhatikan bahwa kompiler yang memenuhi standar ISO C diperlukan untuk menghasilkan peringatan jika konversi dari penunjuk void * ke penunjuk fungsi dicoba seperti pada:

 fptr = (int (*)(int))dlsym(handle, "my_function");

Karena masalah yang dicatat di sini, versi masa depan dapat menambahkan fungsi baru untuk mengembalikan pointer fungsi, atau antarmuka saat ini mungkin tidak digunakan lagi karena dua fungsi baru: yang mengembalikan pointer data dan yang lain mengembalikan pointer fungsi.

Maxim Egorushkin
sumber
apakah itu berarti menggunakan dlsym untuk mendapatkan alamat suatu fungsi saat ini tidak aman? Apakah saat ini ada cara yang aman untuk melakukannya?
gexicide
4
Ini berarti bahwa POSIX saat ini membutuhkan dari platform ABI bahwa fungsi dan pointer data dapat dengan aman dilemparkan ke void*dan kembali.
Maxim Egorushkin
@geksisida Ini berarti bahwa implementasi yang sesuai dengan POSIX telah membuat ekstensi ke bahasa, memberikan makna yang ditentukan implementasi untuk apa perilaku yang tidak terdefinisi sesuai standar masing-masing. Itu bahkan terdaftar sebagai salah satu ekstensi umum untuk standar C99, bagian J.5.7. Fungsi penunjuk pointer.
David Hammen
1
@DavidHammen Ini bukan ekstensi ke bahasa, melainkan persyaratan tambahan baru. C tidak perlu void*kompatibel dengan pointer fungsi, sedangkan POSIX melakukannya.
Maxim Egorushkin
9

C ++ 11 memiliki solusi untuk ketidakcocokan jangka panjang antara C / C ++ dan POSIX dlsym(). Satu dapat digunakan reinterpret_castuntuk mengonversi penunjuk fungsi ke / dari penunjuk data selama implementasinya mendukung fitur ini.

Dari standar, 5.2.10 para. 8, "mengubah pointer fungsi ke tipe pointer objek atau sebaliknya didukung oleh kondisi." 1.3.5 mendefinisikan "yang didukung secara kondisional" sebagai "program yang menyatakan bahwa suatu implementasi tidak diperlukan untuk mendukung".

David Hammen
sumber
Seseorang bisa, tetapi tidak seharusnya. Compiler yang menyesuaikan harus menghasilkan peringatan untuk itu (yang pada gilirannya akan memicu kesalahan, lih -Werror.). Solusi yang lebih baik (dan non-UB) adalah untuk mengambil pointer ke objek yang dikembalikan oleh dlsym(yaitu void**) dan mengubahnya menjadi pointer ke fungsi pointer . Masih implementasi yang ditentukan tetapi tidak lagi menyebabkan peringatan / kesalahan .
Konrad Rudolph
3
@KonradRudolph: Tidak Setuju. Kata-kata "didukung kondisional" secara khusus ditulis untuk memungkinkan dlsymdan GetProcAddressmengkompilasi tanpa peringatan.
MSalters
@ MSalters Apa maksudmu, "tidak setuju"? Entah aku benar atau salah. The dokumentasi dlsym secara eksplisit mengatakan bahwa “kompiler sesuai dengan standar ISO C yang diperlukan untuk menghasilkan peringatan jika konversi dari * pointer kekosongan untuk fungsi pointer dicoba”. Ini tidak meninggalkan banyak ruang untuk spekulasi. Dan GCC (dengan -pedantic) tidak memperingatkan. Sekali lagi, tidak ada spekulasi yang mungkin.
Konrad Rudolph
1
Tindak lanjut: Saya pikir sekarang saya mengerti. Itu bukan UB. Ini didefinisikan implementasi. Saya masih tidak yakin apakah peringatan harus dibuat atau tidak - mungkin tidak. Baiklah.
Konrad Rudolph
2
@KonradRudolph: Saya tidak setuju dengan "seharusnya" Anda, yang merupakan pendapat. Jawabannya secara khusus disebutkan C ++ 11, dan saya adalah anggota C ++ CWG pada saat masalah tersebut diatasi. C99 memang memiliki kata-kata yang berbeda, didukung oleh kondisi adalah penemuan C ++.
MSalters
7

Bergantung pada arsitektur target, kode dan data dapat disimpan di area memori yang secara fundamental tidak kompatibel dan berbeda secara fisik.

Graham Borland
sumber
'secara fisik berbeda' saya mengerti, tetapi dapatkah Anda menjelaskan lebih lanjut tentang perbedaan 'yang secara fundamental tidak sesuai'. Seperti yang saya katakan dalam pertanyaan, bukankah void pointer seharusnya sebesar jenis pointer apa pun - atau apakah itu anggapan yang salah di pihak saya.
Manav
@KnickerKicker: void *cukup besar untuk menampung data pointer apa pun, tetapi tidak harus fungsi pointer apa pun.
ephemient
1
kembali ke masa depan: P
SSpoke
5

undefined tidak selalu berarti tidak diperbolehkan, itu dapat berarti bahwa implementator kompiler memiliki lebih banyak kebebasan untuk melakukannya seperti yang mereka inginkan.

Sebagai contoh, beberapa arsitektur mungkin tidak dimungkinkan - undefined memungkinkan mereka untuk tetap memiliki pustaka 'C' yang sesuai bahkan jika Anda tidak dapat melakukan ini.

Martin Beckett
sumber
5

Solusi lain:

Dengan asumsi POSIX menjamin fungsi dan pointer data memiliki ukuran dan representasi yang sama (saya tidak dapat menemukan teks untuk ini, tetapi contoh yang dikutip OP menyarankan mereka setidaknya bermaksud membuat persyaratan ini), berikut ini harus berfungsi:

double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);

Ini menghindari melanggar aturan aliasing dengan melalui char [] representasi, yang diizinkan untuk alias semua tipe.

Namun pendekatan lain:

union {
    double (*fptr)(double);
    void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;

Tetapi saya akan merekomendasikan memcpypendekatan jika Anda ingin benar-benar 100% benar C.

R .. GitHub BERHENTI MEMBANTU ICE
sumber
5

Mereka dapat menjadi tipe yang berbeda dengan kebutuhan ruang yang berbeda. Menetapkan ke salah satu dapat irreversible mengiris nilai pointer sehingga memberikan hasil yang berbeda.

Saya percaya mereka dapat jenis yang berbeda karena standar tidak ingin membatasi kemungkinan implementasi yang menghemat ruang saat tidak diperlukan atau ketika ukuran dapat menyebabkan CPU harus melakukan omong kosong tambahan untuk menggunakannya, dll ...

Edward Strange
sumber
3

Satu-satunya solusi yang benar-benar portabel adalah tidak digunakan dlsymuntuk fungsi, dan sebaliknya digunakan dlsymuntuk mendapatkan pointer ke data yang berisi pointer fungsi. Misalnya, di perpustakaan Anda:

struct module foo_module = {
    .create = create_func,
    .destroy = destroy_func,
    .write = write_func,
    /* ... */
};

dan kemudian di aplikasi Anda:

struct module *foo = dlsym(handle, "foo_module");
foo->create(/*...*/);
/* ... */

Kebetulan, ini adalah praktik desain yang baik, dan membuatnya mudah untuk mendukung pemuatan dinamis melalui dlopendan statis menghubungkan semua modul pada sistem yang tidak mendukung tautan dinamis, atau di mana pengguna / integrator sistem tidak ingin menggunakan tautan dinamis.

R .. GitHub BERHENTI MEMBANTU ICE
sumber
2
Bagus! Sementara saya setuju ini tampaknya lebih dapat dipertahankan, masih belum jelas (bagi saya) bagaimana saya palu pada tautan statis di atas ini. Bisakah Anda menguraikan?
Manav
2
Jika setiap modul memiliki foo_modulestrukturnya sendiri (dengan nama unik), Anda dapat membuat file tambahan dengan array struct { const char *module_name; const struct module *module_funcs; }dan fungsi sederhana untuk mencari tabel ini untuk modul yang ingin Anda "muat" dan mengembalikan penunjuk yang benar, kemudian gunakan ini di tempat dlopendan dlsym.
R .. GitHub BERHENTI MEMBANTU ICE
@ R .. Benar, tetapi menambah biaya perawatan dengan harus mempertahankan struktur modul.
user877329
3

Contoh modern di mana fungsi pointer dapat berbeda ukurannya dari pointer data: pointer fungsi anggota kelas C ++

Dikutip langsung dari https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/

class Base1 { int b1; void Base1Method(); };
class Base2 { int b2; void Base2Method(); };
class Derived : public Base1, Base2 { int d; void DerivedMethod(); };

Sekarang ada dua kemungkinan thispetunjuk.

Pointer ke fungsi anggota Base1dapat digunakan sebagai pointer ke fungsi anggota Derived, karena keduanya menggunakan this pointer yang sama . Tetapi pointer ke fungsi anggota Base2tidak dapat digunakan apa adanya sebagai pointer ke fungsi anggota Derived, karena this pointer perlu disesuaikan.

Ada banyak cara untuk menyelesaikan ini. Inilah cara kompiler Visual Studio memutuskan untuk menanganinya:

Pointer ke fungsi anggota dari kelas multiply-inherited adalah struktur.

[Address of function]
[Adjustor]

Ukuran fungsi pointer-ke-anggota kelas yang menggunakan multiple inheritance adalah ukuran pointer plus ukuran a size_t.

tl; dr: Saat menggunakan banyak pewarisan, penunjuk ke fungsi anggota dapat (tergantung pada kompiler, versi, arsitektur, dll) sebenarnya disimpan sebagai

struct { 
    void * func;
    size_t offset;
}

yang jelas lebih besar dari a void *.

Andrew Sun
sumber
2

Pada sebagian besar arsitektur, pointer ke semua tipe data normal memiliki representasi yang sama, jadi casting di antara tipe-tipe pointer data adalah no-op.

Namun, bisa dibayangkan bahwa pointer fungsi mungkin memerlukan representasi yang berbeda, mungkin mereka lebih besar dari pointer lainnya. Jika void * dapat menyimpan fungsi pointer, ini berarti bahwa representasi void * harus berukuran lebih besar. Dan semua cast data pointer ke / dari void * harus melakukan salinan tambahan ini.

Sebagai seseorang yang disebutkan, jika Anda membutuhkan ini, Anda dapat mencapainya menggunakan serikat pekerja. Tetapi sebagian besar penggunaan void * hanya untuk data, jadi akan sulit untuk meningkatkan semua penggunaan memori mereka kalau-kalau pointer fungsi perlu disimpan.

Barmar
sumber
-1

Saya tahu bahwa ini belum mengomentari sejak 2012, tapi saya pikir itu akan berguna untuk menambahkan bahwa saya lakukan tahu arsitektur yang memiliki sangat pointer tidak kompatibel untuk data dan fungsi karena panggilan pada yang cek arsitektur hak istimewa dan membawa informasi tambahan. Tidak ada jumlah casting yang akan membantu. Itu The Mill .

phorgan1
sumber
Jawaban ini salah. Misalnya Anda dapat mengonversi penunjuk fungsi ke penunjuk data dan membaca darinya (jika Anda memiliki izin untuk membaca dari alamat itu, seperti biasa). Hasilnya masuk akal seperti halnya misalnya pada x86.
Manuel Jacob