Mencetak pointer nol dengan% p adalah perilaku tidak terdefinisi?

93

Apakah perilaku tidak terdefinisi untuk mencetak pointer nol dengan penentu %pkonversi?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

Pertanyaannya berlaku untuk standar C, dan bukan untuk implementasi C.

Dror K.
sumber
A tidak benar-benar berpikir bahwa ada orang (termasuk komite C) yang terlalu peduli tentang itu. Ini adalah masalah yang cukup artifisial, tanpa (atau hampir tidak ada) signifikansi praktisnya.
P__J mendukung wanita di Polandia
itu karena printf hanya menampilkan nilai, dan tidak menyentuh (dalam arti membaca atau menulis objek runcing) - tidak dapat UB i pointer memiliki valid untuk nilai tipenya (NULL adalah nilai yang valid )
P__J mendukung wanita di Polandia
3
@PeterJ katakanlah apa yang Anda katakan itu benar (meskipun jelas standar menyatakan sebaliknya), faktanya saja, bahwa kami memperdebatkan hal ini membuat pertanyaan menjadi valid dan benar, karena sepertinya bagian yang dikutip di bawah dari standar membuat sangat sulit untuk memahami untuk pengembang biasa apa yang sedang terjadi .. Artinya: pertanyaan tidak pantas mendapat suara turun, karena masalah ini memerlukan klarifikasi!
Peter Varo
2
@PeterJ itu cerita yang berbeda, terima kasih atas klarifikasinya :)
Peter Varo

Jawaban:

93

Ini adalah salah satu kasus sudut aneh di mana kami tunduk pada batasan bahasa Inggris dan struktur yang tidak konsisten dalam standar. Jadi paling banter, saya bisa membuat argumen tandingan yang meyakinkan, karena tidak mungkin membuktikannya :) 1


Kode dalam pertanyaan menunjukkan perilaku yang terdefinisi dengan baik.

Karena [7.1.4] adalah dasar pertanyaannya, mari kita mulai dari sana:

Masing-masing pernyataan berikut berlaku kecuali secara eksplisit dinyatakan lain dalam deskripsi mendetail yang mengikuti: Jika argumen ke suatu fungsi memiliki nilai yang tidak valid ( seperti nilai di luar domain fungsi, atau penunjuk di luar ruang alamat program, atau pointer nol , [... contoh lain ...] ) [...] perilaku tidak ditentukan. [... pernyataan lain ...]

Ini adalah bahasa yang kikuk. Salah satu interpretasinya adalah bahwa item dalam daftar adalah UB untuk semua fungsi perpustakaan, kecuali diganti dengan deskripsi individu. Namun daftar tersebut dimulai dengan "seperti", yang menunjukkan bahwa ini ilustrasi, bukan lengkap. Misalnya, ia tidak menyebutkan penghentian null string yang benar (penting untuk perilaku misalnya strcpy).

Jadi jelas maksud / ruang lingkup 7.1.4 hanyalah bahwa "nilai tidak valid" mengarah ke UB ( kecuali dinyatakan lain ). Kita harus melihat deskripsi setiap fungsi untuk menentukan apa yang dianggap sebagai "nilai tidak valid".

Contoh 1 - strcpy

[7.21.2.3] hanya mengatakan ini:

The strcpysalinan fungsi string yang ditunjukkan oleh s2(termasuk karakter null terminating) ke dalam array yang ditunjukkan oleh s1. Jika penyalinan terjadi di antara objek yang tumpang tindih, perilakunya tidak ditentukan.

Itu tidak menyebutkan secara eksplisit pointer nol, namun juga tidak menyebutkan terminator nol. Sebaliknya, seseorang menyimpulkan dari "string yang ditunjukkan oleh s2" bahwa satu-satunya nilai yang valid adalah string (yaitu, penunjuk ke array karakter yang diakhiri dengan null).

Memang, pola ini dapat dilihat di seluruh deskripsi individu. Beberapa contoh lain:

  • [7.6.4.1 (fenv)] menyimpan lingkungan floating-point saat ini di objek yang ditunjuk olehenvp

  • [7.12.6.4 (frexp)] menyimpan integer dalam obyek int yang ditunjuk olehexp

  • [7.19.5.1 (fclose)] yang aliran menunjuk olehstream

Contoh 2 - printf

[7.19.6.1] mengatakan ini tentang %p:

p- Argumen harus menjadi petunjuk void. Nilai penunjuk diubah menjadi urutan karakter pencetakan, dengan cara yang ditentukan implementasi.

Null adalah nilai pointer yang valid, dan bagian ini tidak menyebutkan secara eksplisit bahwa null adalah kasus khusus, atau pointer harus menunjuk ke suatu objek. Dengan demikian itu didefinisikan perilaku.


1. Kecuali jika penulis standar tampil ke depan, atau kecuali kita dapat menemukan sesuatu yang mirip dengan dokumen alasan yang menjelaskan hal-hal.

Oliver Charlesworth
sumber
Komentar tidak untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Bhargav Rao
1
"namun tidak menyebutkan terminator nol" lemah dalam Contoh 1 - strcpy karena spesifikasi mengatakan "menyalin string ". string secara eksplisit didefinisikan sebagai memiliki karakter null .
chux - Kembalikan Monica
1
@chux - Itulah maksud saya - seseorang harus menyimpulkan apa yang valid / tidak valid dari konteks, daripada berasumsi bahwa daftar di 7.1.4 lengkap. (Namun, keberadaan bagian dari jawaban saya ini lebih masuk akal dalam konteks komentar yang telah dihapus, dengan alasan bahwa strcpy adalah contoh yang berlawanan.)
Oliver Charlesworth
1
Inti dari masalah ini adalah bagaimana pembaca akan menafsirkan seperti . Apakah itu berarti beberapa contoh kemungkinan nilai tidak valid ? Apakah itu berarti beberapa contoh yang selalu bernilai tidak valid ? Sebagai catatan, saya pergi dengan interpretasi pertama.
ninjalj
1
@ninjalj - Ya, setuju. Itulah intinya yang ingin saya sampaikan dalam jawaban saya di sini, yaitu "ini adalah contoh jenis hal yang mungkin nilainya tidak valid". :)
Oliver Charlesworth
20

Jawaban Singkat

Iya . Mencetak pointer nol dengan penentu %pkonversi memiliki perilaku yang tidak ditentukan. Karena itu, saya tidak mengetahui adanya implementasi yang sesuai yang akan berperilaku tidak semestinya.

Jawabannya berlaku untuk salah satu standar C (C89 / C99 / C11).


Jawaban Panjang

The %pkonversi specifier mengharapkan argumen dari jenis pointer ke kekosongan, konversi dari pointer ke karakter dicetak adalah pelaksanaan yang ditetapkan. Itu tidak menyatakan bahwa pointer nol diharapkan.

Pengenalan fungsi pustaka standar menyatakan bahwa penunjuk nol sebagai argumen untuk fungsi (pustaka standar) dianggap sebagai nilai yang tidak valid, kecuali jika dinyatakan lain secara eksplisit.

C99 / C11 §7.1.4 p1

[...] Jika argumen ke suatu fungsi memiliki nilai yang tidak valid (seperti [...] pointer nol, [...] perilakunya tidak terdefinisi.

Contoh untuk fungsi (pustaka standar) yang mengharapkan penunjuk nol sebagai argumen yang valid:

  • fflush() menggunakan pointer null untuk membersihkan "semua aliran" (yang berlaku).
  • freopen() menggunakan pointer nol untuk menunjukkan file "saat ini terkait" dengan aliran.
  • snprintf() memungkinkan untuk melewatkan pointer nol ketika 'n' adalah nol.
  • realloc() menggunakan pointer nol untuk mengalokasikan objek baru.
  • free() memungkinkan untuk melewatkan pointer nol.
  • strtok() menggunakan pointer nol untuk panggilan berikutnya.

Jika kita mengambil kasus untuk snprintf(), masuk akal untuk mengizinkan melewatkan pointer nol ketika 'n' adalah nol, tetapi ini tidak berlaku untuk fungsi (perpustakaan standar) lain yang memungkinkan nol 'n' serupa. Sebagai contoh: memcpy(), memmove(), strncpy(), memset(), memcmp().

Ini tidak hanya ditentukan dalam pendahuluan ke pustaka standar, tetapi juga sekali lagi dalam pengantar fungsi berikut:

C99 §7.21.1 p2 / C11 §7.24.1 p2

Di mana argumen yang dideklarasikan sebagai size_tn menentukan panjang array untuk suatu fungsi, n dapat memiliki nilai nol pada panggilan ke fungsi itu. Kecuali secara eksplisit dinyatakan lain dalam deskripsi fungsi tertentu di subayat ini, argumen penunjuk pada panggilan tersebut masih memiliki nilai yang valid seperti yang dijelaskan dalam 7.1.4.


Apakah ini disengaja?

Saya tidak tahu apakah UB %pdengan pointer nol sebenarnya disengaja, tetapi karena standar secara eksplisit menyatakan bahwa pointer nol dianggap nilai tidak valid sebagai argumen untuk fungsi perpustakaan standar, dan kemudian berjalan dan secara eksplisit menentukan kasus-kasus di mana null pointer adalah argumen yang valid (snprintf, bebas, dll), dan kemudian ia pergi dan sekali lagi mengulangi persyaratan untuk argumen akan berlaku bahkan dalam nol 'n' kasus ( memcpy, memmove, memset), maka saya pikir itu masuk akal untuk mengasumsikan bahwa Komite standar C tidak terlalu peduli dengan hal-hal seperti itu yang tidak ditentukan.

Dror K.
sumber
Komentar tidak untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Bhargav Rao
1
@JeroenMostert: Apa maksud dari argumen ini? Kutipan 7.1.4 yang diberikan cukup jelas, bukan? Apa yang bisa diperdebatkan tentang "kecuali secara eksplisit dinyatakan lain" jika tidak dinyatakan lain? Apa yang bisa diperdebatkan tentang fakta bahwa pustaka fungsi string (tidak terkait) memiliki kata-kata yang mirip, jadi kata-kata itu tampaknya tidak aksidental? Saya pikir jawaban ini (meskipun tidak terlalu berguna dalam praktiknya ) adalah benar.
Damon
3
@ Damon: Perangkat keras mitos Anda bukanlah mitos, ada banyak arsitektur di mana nilai-nilai yang tidak mewakili alamat yang valid mungkin tidak dimuat dalam register alamat. Namun, meneruskan pointer nol sebagai argumen fungsi masih diperlukan untuk bekerja pada platform tersebut sebagai mekanisme umum. Hanya meletakkan satu di tumpukan tidak akan meledakkan semuanya.
Jeroen Mostert
1
@anatolyg: Pada prosesor x86, alamat memiliki dua bagian - segmen dan offset. Pada 8086, memuat register segmen seperti memuat yang lain, tetapi pada semua mesin yang lebih baru, itu mengambil deskriptor segmen. Memuat deskriptor yang tidak valid menyebabkan jebakan. Banyak kode untuk 80386 dan prosesor yang lebih baru, bagaimanapun, hanya menggunakan satu segmen, dan dengan demikian tidak pernah memuat register segmen sama sekali .
supercat
1
Saya pikir semua orang akan setuju bahwa mencetak pointer nol dengan %ptidak seharusnya merupakan perilaku yang tidak terdefinisi
MM
-1

Penulis Standar C tidak berupaya untuk membuat daftar secara mendalam semua persyaratan perilaku yang harus dipenuhi oleh implementasi agar sesuai untuk tujuan tertentu. Sebaliknya, mereka berharap bahwa orang-orang yang menulis kompiler akan menggunakan akal sehat tertentu apakah Standar mengharuskannya atau tidak.

Pertanyaan apakah ada sesuatu yang memanggil UB jarang dengan sendirinya berguna. Pertanyaan penting sebenarnya adalah:

  1. Haruskah seseorang yang mencoba menulis kompiler berkualitas membuatnya berperilaku dengan cara yang dapat diprediksi? Untuk skenario yang dijelaskan, jawabannya jelas ya.

  2. Haruskah programmer berhak mengharapkan bahwa kompiler berkualitas untuk apa pun yang menyerupai platform normal akan berperilaku dengan cara yang dapat diprediksi? Dalam skenario yang dijelaskan, saya akan mengatakan jawabannya adalah ya.

  3. Mungkinkah beberapa penulis kompilator yang bodoh meregangkan interpretasi Standar untuk membenarkan melakukan sesuatu yang aneh? Saya berharap tidak, tetapi tidak akan mengesampingkannya.

  4. Haruskah penyusun sanitasi mengoceh tentang perilakunya? Itu akan tergantung pada tingkat paranoia penggunanya; kompiler pembersih mungkin seharusnya tidak default untuk mengoceh tentang perilaku seperti itu, tetapi mungkin menyediakan opsi konfigurasi untuk dilakukan jika program mungkin di-porting ke kompiler "pintar" / bodoh yang berperilaku aneh.

Jika interpretasi yang masuk akal dari Standar akan menyiratkan suatu perilaku yang didefinisikan, tetapi beberapa penulis kompilator memperluas interpretasi untuk membenarkan melakukan sebaliknya, apakah penting apa yang dikatakan Standar?

supercat
sumber
1. Tidak jarang programmer menemukan asumsi yang dibuat oleh pengoptimal modern / agresif bertentangan dengan apa yang mereka anggap "masuk akal" atau "berkualitas". 2. Dalam hal ambiguitas dalam spesifikasi, tidak jarang pelaksana berselisih tentang kebebasan apa yang mereka anggap. 3. Jika menyangkut anggota komite standar C, bahkan mereka tidak selalu setuju dengan interpretasi yang 'benar', apalagi yang seharusnya . Mengingat hal tersebut di atas, interpretasi wajar siapa yang harus kita ikuti?
Dror K.
6
Menjawab pertanyaan "apakah bagian kode ini memanggil UB atau tidak" dengan disertasi tentang apa yang Anda pikirkan tentang kegunaan UB atau bagaimana seharusnya penyusun berperilaku adalah upaya yang buruk untuk menjawab, terutama karena Anda dapat menyalin-tempel ini sebagai jawaban atas hampir semua pertanyaan tentang UB tertentu. Sebagai pengingat untuk perkembangan retoris Anda: ya, sangat penting apa yang dikatakan Standar, tidak peduli apa yang dilakukan beberapa penulis kompilator atau pendapat Anda tentang mereka untuk melakukan itu, karena Standar adalah awal mula pemrogram dan penulis kompilator.
Jeroen Mostert
1
@JeroenMostert: Jawaban untuk "Apakah X meminta perilaku yang tidak ditentukan" akan sering bergantung pada apa yang dimaksud dengan pertanyaan tersebut. Jika suatu program dianggap memiliki Perilaku Tidak Terdefinisi jika Standar tidak memberlakukan persyaratan apa pun pada perilaku penerapan yang sesuai, maka hampir semua program meminta UB. Penulis Standar dengan jelas mengizinkan implementasi untuk berperilaku sewenang-wenang jika sebuah program menumpuk panggilan fungsi terlalu dalam, selama implementasi dapat dengan benar memproses setidaknya satu teks sumber (yang mungkin dibuat-buat) yang melatih batas terjemahan di Stadard.
supercat
@supercat: sangat menarik, tetapi apakah printf("%p", (void*) 0)perilaku tidak terdefinisi atau tidak, menurut Standar? Panggilan fungsi yang sangat bertingkat sama relevannya dengan harga teh di China. Dan ya, UB sangat umum dalam program dunia nyata - bagaimana dengan itu?
Jeroen Mostert
1
@JeroenMostert: Karena Standar akan memungkinkan implementasi yang tumpul untuk menganggap hampir semua program memiliki UB, yang seharusnya menjadi masalah adalah perilaku implementasi yang tidak tumpul. Jika Anda tidak menyadarinya, saya tidak hanya menulis copy / paste tentang UB, tetapi menjawab pertanyaan tentang %puntuk setiap kemungkinan arti dari pertanyaan tersebut.
supercat