Kapan Undefined Behavior in C melompati penghalang kausalitas


8

Beberapa kompiler C hiper-modern akan menyimpulkan bahwa jika suatu program akan memanggil Perilaku Tidak Terdefinisi ketika diberi input tertentu, input seperti itu tidak akan pernah diterima. Akibatnya, kode apa pun yang tidak relevan kecuali jika input tersebut diterima dapat dihilangkan.

Sebagai contoh sederhana, diberikan:

void foo(uint32_t);

uint32_t rotateleft(uint_t value, uint32_t amount)
{
  return (value << amount) | (value >> (32-amount));
}

uint32_t blah(uint32_t x, uint32_t y)
{
  if (y != 0) foo(y);
  return rotateleft(x,y);
}

seorang kompiler dapat menyimpulkan bahwa karena evaluasi value >> (32-amount)akan menghasilkan Perilaku Tidak Terdefinisi ketika amountnol, fungsi blahtidak akan pernah dipanggil dengan ysama dengan nol; panggilan untuk foodemikian bisa dibuat tanpa syarat.

Dari apa yang bisa saya katakan, filosofi ini tampaknya telah bertahan sekitar tahun 2010. Bukti paling awal yang saya lihat dari akarnya kembali ke tahun 2009, dan itu telah diabadikan dalam standar C11 yang secara eksplisit menyatakan bahwa jika Perilaku Tidak Terdefinisi terjadi pada setiap titik dalam pelaksanaan program, perilaku seluruh program secara surut menjadi tidak terdefinisi.

Apakah gagasan bahwa kompiler harus berusaha untuk menggunakan Perilaku Undefined untuk membenarkan optimasi reverse-kausal (yaitu Perilaku Undefined di rotateleftfungsi harus menyebabkan compiler untuk mengasumsikan bahwa blahharus telah disebut dengan non-nol y, apakah tidak apa-apa akan pernah menyebabkan yuntuk memegang nilai tidak nol) secara serius diadvokasi sebelum 2009? Kapan hal seperti itu pertama kali diusulkan secara serius sebagai teknik optimasi?

[Tambahan]

Beberapa kompiler telah, bahkan di Abad ke-20, menyertakan opsi untuk memungkinkan beberapa jenis kesimpulan tentang loop dan nilai yang dihitung di dalamnya. Misalnya diberikan

int i; int total=0;
for (i=n; i>=0; i--)
{
  doSomething();
  total += i*1000;
}

kompiler, bahkan tanpa inferensi opsional, dapat menulis ulang sebagai:

int i; int total=0; int x1000;
for (i=n, x1000=n*1000; i>0; i--, x1000-=1000)
{
  doSomething();
  total += x1000;
}

karena perilaku kode itu akan sama persis dengan yang asli, bahkan jika kompilator menentukan bahwa intnilai selalu membungkus mod-65536 mode dua-pelengkap . Opsi tambahan-inferensi akan membiarkan kompiler mengenali bahwa karena idan x1000harus melewati nol pada saat yang sama, variabel sebelumnya dapat dihilangkan:

int total=0; int x1000;
for (x1000=n*1000; x1000 > 0; x1000-=1000)
{
  doSomething();
  total += x1000;
}

Pada sistem di mana intnilai dibungkus mod 65536, upaya untuk menjalankan salah satu dari dua loop pertama dengan n33 akan menghasilkan doSomething()dipanggil 33 kali. Loop terakhir, sebaliknya, tidak akan meminta doSomething()sama sekali, meskipun doa pertama doSomething()akan mendahului aritmatika melimpah. Perilaku seperti mungkin dianggap "non-kausal", tetapi efek yang cukup baik dibatasi dan ada banyak kasus di mana perilaku akan terbukti berbahaya (dalam kasus di mana fungsi yang diperlukan untuk menghasilkan beberapa nilai ketika diberikan setiap masukan, tetapi nilai mungkin sewenang-wenang jika input tidak valid, memiliki loop selesai lebih cepat ketika diberi nilai tidak valid sebesarnsebenarnya akan bermanfaat). Lebih lanjut, dokumentasi kompiler cenderung meminta maaf karena fakta bahwa itu akan mengubah perilaku program apa pun - bahkan yang terlibat di UB.

Saya tertarik ketika sikap penulis kompiler berubah dari gagasan bahwa platform harus ketika mendokumentasikan beberapa kendala perilaku yang dapat digunakan bahkan dalam kasus-kasus yang tidak diamanatkan oleh Standar, dengan gagasan bahwa setiap konstruksi yang akan bergantung pada perilaku yang tidak diamanatkan oleh Standar harus dicap tidak sah bahkan jika pada kebanyakan kompiler yang ada itu akan berfungsi dengan baik atau lebih baik daripada kode yang memenuhi persyaratan yang sama memenuhi persyaratan yang sama (sering memungkinkan optimasi yang tidak mungkin dilakukan dalam kode yang sepenuhnya mematuhi).


Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
maple_shaft

1
@ rwong: Saya tidak melihat bagaimana itu bahkan melanggar hukum waktu, apalagi hubungan sebab akibat. Jika gips statis yang tidak sah menghentikan eksekusi program (bukan model eksekusi yang tidak jelas), dan jika model build sedemikian rupa sehingga tidak ada kelas tambahan yang dapat didefinisikan setelah menautkan, maka tidak ada input ke program yang dapat menyebabkan shape->Is2D()dipanggil pada objek yang tidak diturunkan dari Shape2D. Ada perbedaan besar antara mengoptimalkan kode yang hanya akan relevan jika Perilaku Undefined kritis telah terjadi dibandingkan kode yang hanya akan relevan dalam kasus-kasus di mana ...
supercat

... tidak ada jalur eksekusi hilir yang tidak meminta beberapa bentuk Perilaku Tidak Terdefinisi. Sementara definisi "perilaku tidak terdefinisi kritis" yang diberikan dalam (IIRC) Annex L agak kabur, suatu implementasi mungkin menerapkan pengecoran statis dan pengiriman virtual sedemikian rupa sehingga kode yang ditunjukkan akan melompat ke alamat yang berubah-ubah (itu penting UB). Untuk itu selalu melompat ke Shape2D::Is­2Dsebenarnya lebih baik daripada program yang layak.
supercat

1
@ rwong: Mengenai duplikatnya, maksud pertanyaan saya adalah menanyakan kapan dalam sejarah manusia interpretasi normatif dari Perilaku Tidak Terdefinisi sehubungan dengan misalnya melimpah bergeser dari "Jika perilaku alami platform akan memungkinkan aplikasi memenuhi persyaratannya tanpa pengecekan kesalahan , memiliki mandat standar setiap perilaku yang bertentangan dapat meningkatkan kompleksitas kode sumber, kode yang dapat dieksekusi, dan kode kompiler yang diperlukan untuk memiliki program yang memenuhi persyaratan, sehingga Standar seharusnya membiarkan platform melakukan apa yang ia lakukan dengan sebaik-baiknya "hingga" Para penyusun harus berasumsi programmer itu ...
supercat

1
... akan selalu menyertakan kode untuk mencegah luapan dalam kasus apa pun di mana mereka tidak ingin meluncurkan rudal nuklir. "Secara historis, diberikan spesifikasi" tulis fungsi yang, diberikan dua argumen tipe 'int', mengembalikan produk jika direpresentasikan sebagai 'int', atau jika tidak menghentikan program atau mengembalikan nomor sewenang-wenang ", int prod(int x, int y) {return x*y;}akan mencukupi. Mematuhi" jangan meluncurkan nuklir "dengan cara yang benar-benar sesuai, bagaimanapun, akan memerlukan kode yang lebih sulit untuk dibaca dan hampir akan tentu berjalan jauh lebih lambat di banyak platform
supercat

Jawaban:


4

Perilaku tidak terdefinisi digunakan dalam situasi di mana tidak layak bagi spec untuk menentukan perilaku, dan selalu ditulis untuk memungkinkan benar-benar perilaku yang mungkin.

Aturan yang sangat longgar untuk UB sangat membantu ketika Anda berpikir tentang apa yang harus melalui kompiler penyesuai spesifikasi. Anda mungkin memiliki daya kuda kompilasi yang cukup untuk mengeluarkan kesalahan ketika Anda melakukan UB yang buruk dalam satu kasus, tetapi menambahkan beberapa lapisan rekursi dan sekarang yang terbaik yang dapat Anda lakukan adalah peringatan. Spec tidak memiliki konsep "peringatan," jadi jika spec telah memberikan perilaku, itu harus menjadi "kesalahan."

Alasan kita melihat semakin banyak efek samping dari ini adalah dorongan untuk optimasi. Menulis spec optimizer yang sesuai sulit. Menulis pengoptimal konform spesifikasi yang juga terjadi untuk melakukan pekerjaan yang sangat baik menebak apa yang Anda maksudkan ketika Anda pergi ke luar spek itu brutal. Lebih mudah pada kompiler jika mereka menganggap UB berarti UB.

Ini terutama berlaku untuk gcc, yang mencoba mendukung banyak set instruksi dengan kompiler yang sama. Jauh lebih mudah untuk membiarkan UB menghasilkan perilaku UB daripada mencoba untuk bergulat dengan semua cara setiap kode UB bisa salah di setiap platform, dan memasukkannya ke dalam frasa awal pengoptimal.


1
Alasan C mendapatkan reputasinya untuk kecepatan adalah karena programmer yang tahu bahwa bahkan dalam beberapa kasus di mana Standar tidak memberlakukan persyaratan, perilaku platform mereka akan memenuhi persyaratan mereka dapat memanfaatkan perilaku tersebut. Jika suatu platform menjamin bahwa x-y > zsewenang-wenang akan menghasilkan 0 atau 1 ketika x-ytidak dapat dianggap sebagai "int", platform tersebut akan memiliki lebih banyak peluang pengoptimalan daripada platform yang mengharuskan ekspresi ditulis sebagai salah satu UINT_MAX/2+1+x+y > UINT_MAX/2+1+zatau (long long)x+y > z.
supercat

1
Sebelum kira-kira tahun 2009, interpretasi khas dari banyak bentuk UB akan menjadi misalnya "Bahkan jika menjalankan instruksi shift-kanan-oleh-N pada CPU target dapat menunda bus selama beberapa menit dengan interupsi dinonaktifkan ketika N adalah -1 [I sudah pernah mendengar CPU semacam itu], sebuah kompiler dapat meneruskan setiap N ke instruksi shift-right-by-N tanpa validasi, atau mask N dengan 31, atau secara sewenang-wenang memilih di antara opsi-opsi itu ". Dalam kasus di mana salah satu perilaku di atas akan memenuhi persyaratan, programmer tidak perlu membuang waktu (milik mereka atau mesin) mencegah mereka terjadi. Apa yang mendorong perubahan itu?
supercat

@supercat saya akan mengatakan bahwa, bahkan pada tahun 2015, interpretasi "khas" UB akan sama jinaknya. Masalahnya bukan kasus khas UB, ini kasus yang tidak biasa. Saya akan mengatakan 95-98% dari UB Saya tidak sengaja menulis "bekerja sebagaimana dimaksud." Ini adalah 2-5% sisanya di mana Anda tidak dapat memahami mengapa UB begitu tak terkendali sampai Anda menghabiskan 3 tahun menggali dalam-dalam di perut pengoptimal gcc untuk menyadari bahwa itu sebenarnya sama anehnya dengan kasus yang mereka kira ... awalnya terlihat mudah.
Cort Ammon

Contoh yang sangat bagus yang saya temui adalah pelanggaran One Definition Rule (ODR) yang saya tulis secara tidak sengaja, menamai dua kelas dengan hal yang sama, dan mendapatkan bashing stack parah ketika saya memanggil satu fungsi dan menjalankan yang lainnya. Tampaknya masuk akal untuk mengatakan "mengapa Anda tidak menggunakan nama kelas terdekat saja, atau paling tidak memperingatkan saya," sampai Anda memahami bagaimana penghubung menyelesaikan masalah penamaan. Hanya tidak ada opsi perilaku "baik" yang tersedia kecuali mereka menempatkan sejumlah besar kode tambahan dalam kompiler untuk menangkapnya.
Cort Ammon

Teknologi Linker pada umumnya mengerikan, tetapi ada hambatan besar untuk meningkatkannya. Kalau tidak, apa yang saya pikir diperlukan adalah untuk C untuk menstandarisasi seperangkat portabilitas / optimasi typedefs dan makro sedemikian rupa sehingga kompiler yang ada yang mendukung perilaku tersirat sehingga dapat mendukung standar baru hanya dengan menambahkan file header, dan kode yang ingin menggunakan yang baru fitur dapat digunakan dengan kompiler lama yang mendukung perilaku hanya dengan memasukkan file header "kompatibilitas", tetapi kompiler kemudian dapat mencapai optimasi yang jauh lebih baik ...
supercat

4

"Perilaku tidak terdefinisi mungkin menyebabkan kompiler untuk menulis ulang kode" telah terjadi sejak lama, dalam optimisasi loop.

Ambil satu lingkaran (a dan b adalah penunjuk untuk digandakan, misalnya)

for (i = 0; i < n; ++i) a [i] = b [i];

Kami menambah int, kami menyalin elemen array, kami membandingkan dengan batas. Kompilator pengoptimal pertama-tama menghapus pengindeksan:

double* tmp1 = a;
double* tmp2 = b;
for (i = 0; i < n; ++i) *tmp1++ = *tmp2++;

Kami menghapus kasing n <= 0:

i = 0;
if (n > 0) {
    double* tmp1 = a;
    double* tmp2 = b;
    for (; i < n; ++i) *tmp1++ = *tmp2++;
}

Sekarang kita menghilangkan variabel i:

i = 0;
if (n > 0) {
    double* tmp1 = a;
    double* tmp2 = b;
    double* limit = tmp1 + n;
    for (; tmp1 != limit; tmp1++, tmp2++) *tmp1 = *tmp2;
    i = n;
}

Sekarang jika n = 2 ^ 29 pada sistem 32 bit atau 2 ^ 61 pada sistem 64 bit, pada implementasi tipikal kita akan memiliki batas tmp1 ==, dan tidak ada kode yang dieksekusi. Sekarang ganti tugas dengan sesuatu yang membutuhkan waktu lama sehingga kode asli tidak akan pernah mengalami crash yang tak terhindarkan karena terlalu lama, dan kompiler telah mengubah kode.


Jika kedua pointer tidak stabil, saya tidak akan menganggap itu sebagai penulisan ulang kode. Compiler telah lama diizinkan untuk melakukan resequence terhadap non- volatilepointer, sehingga perilaku dalam kasus di mana nbegitu besar yang akan dibungkus pointer akan setara dengan memiliki toko di luar batas yang menyimpan lokasi penyimpanan sementara yang dipegang isebelum yang lainnya. terjadi Jika aatau bvolatile, platform mendokumentasikan bahwa akses volatile menghasilkan operasi beban / toko fisik dalam urutan yang diminta, dan platform mendefinisikan setiap sarana yang
melaluinya

... secara deterministik dapat mempengaruhi eksekusi program, maka saya berpendapat bahwa kompiler harus menahan diri dari optimasi yang ditunjukkan (meskipun saya akan dengan mudah mengakui bahwa beberapa mungkin tidak diprogram untuk mencegah optimasi seperti iitu kecuali jika juga dibuat tidak stabil). Itu akan menjadi kasus sudut perilaku yang cukup langka. Jika adan btidak mudah menguap, saya akan menyarankan bahwa tidak akan ada makna yang dimaksudkan masuk akal untuk apa yang harus dilakukan kode jika nsangat besar untuk menimpa semua memori. Sebaliknya, banyak bentuk lain dari UB memiliki makna yang dimaksudkan masuk akal.
supercat

Atas pertimbangan lebih lanjut, saya dapat memikirkan situasi di mana asumsi tentang hukum bilangan bulat aritmatika dapat menyebabkan loop untuk mengakhiri sebelum waktunya sebelum UB terjadi dan tanpa loop melakukan perhitungan alamat yang tidak sah. Dalam FORTRAN, ada perbedaan yang jelas antara variabel kontrol loop dan variabel lainnya, tetapi C tidak memiliki perbedaan itu, yang tidak menguntungkan karena mengharuskan programmer untuk selalu mencegah overflow pada variabel kontrol loop akan jauh lebih sedikit hambatan untuk pengkodean efisien daripada harus mencegah overflow dimana saja.
supercat

Saya bertanya-tanya bagaimana aturan bahasa dapat ditulis sehingga kode yang akan senang dengan berbagai perilaku jika terjadi luapan bisa mengatakan itu, tanpa menghalangi optimasi yang benar-benar bermanfaat? Ada banyak kasus di mana kode harus menghasilkan output yang didefinisikan secara ketat dengan input yang valid, tetapi output yang didefinisikan secara longgar ketika diberi input yang tidak valid. Misalkan seorang programmer yang akan cenderung untuk menulis if (x-y>z) do_something(); `tidak peduli apakah do_somethingdieksekusi dalam kasus overflow, asalkan overflow tidak memiliki efek lain. Apakah ada cara untuk menulis ulang di atas yang tidak akan ...
supercat

... menghasilkan kode yang tidak efisien dan tidak perlu pada setidaknya beberapa platform (dibandingkan dengan platform apa yang dapat dihasilkan jika diberikan pilihan bebas untuk diminta atau tidak do_something)? Bahkan jika optimasi loop dilarang menghasilkan perilaku yang tidak konsisten dengan model overflow yang longgar, programmer dapat menulis kode sedemikian rupa sehingga memungkinkan kompiler untuk menghasilkan kode optimal. Apakah ada cara untuk mengatasi inefisiensi yang dipaksakan oleh model "hindari overflow di semua biaya"?
supercat

3

Selalu menjadi kasus dalam C dan C ++ bahwa sebagai akibat dari perilaku yang tidak terdefinisi, apa pun bisa terjadi. Oleh karena itu juga selalu menjadi kasus bahwa kompiler dapat membuat asumsi bahwa kode Anda tidak memunculkan perilaku tidak terdefinisi: Entah tidak ada perilaku yang tidak terdefinisi dalam kode Anda, maka anggapan itu benar. Atau ada perilaku yang tidak terdefinisi dalam kode Anda, maka apa pun yang terjadi sebagai akibat dari asumsi yang salah dicakup oleh " apa pun bisa terjadi".

Jika Anda melihat fitur "pembatasan" dalam C, inti keseluruhan dari fitur ini adalah bahwa kompiler dapat mengasumsikan tidak ada perilaku yang tidak terdefinisi, jadi di sana kami mencapai titik di mana kompiler tidak hanya dapat tetapi sebenarnya harus berasumsi tidak ada yang tidak terdefinisi tingkah laku.

Pada contoh yang Anda berikan, instruksi assembler yang biasanya digunakan pada komputer berbasis x86 untuk menerapkan shift kiri atau kanan akan bergeser 0 bit jika jumlah shift 32 untuk kode 32 bit atau 64 untuk kode 64 bit. Ini dalam kebanyakan kasus praktis akan mengarah pada hasil yang tidak diinginkan (dan hasil yang tidak sama seperti pada ARM atau PowerPC, misalnya), sehingga kompiler cukup dibenarkan untuk menganggap bahwa perilaku tidak terdefinisi semacam ini tidak terjadi. Anda dapat mengubah kode Anda menjadi

uint32_t rotateleft(uint_t value, uint32_t amount)
{
   return amount == 0 ? value : (value << amount) | (value >> (32-amount));
}

dan menyarankan kepada pengembang gcc atau Dentang bahwa pada kebanyakan prosesor kode "jumlah == 0" harus dihapus oleh kompiler, karena kode assembler yang dihasilkan untuk kode shift akan menghasilkan hasil yang sama dengan nilai ketika jumlah == 0.


1
Saya bisa memikirkan sangat sedikit platform di mana implementasi x>>y[untuk unsigned x] yang akan berfungsi ketika variabel ymemiliki nilai dari 0 hingga 31, dan melakukan sesuatu selain menghasilkan 0 atau x>>(y & 31)untuk nilai-nilai lain, bisa seefisien satu yang melakukan sesuatu yang lain ; Saya tahu tidak ada platform di mana menjamin bahwa tidak ada tindakan selain dari salah satu di atas akan terjadi akan menambah biaya yang signifikan. Gagasan bahwa programmer harus menggunakan beberapa formulasi yang lebih rumit dalam kode yang tidak akan pernah harus dijalankan pada mesin yang tidak jelas akan dipandang sebagai tidak masuk akal.
supercat

3
Pertanyaan saya adalah kapan hal-hal bergeser dari "x >> 32` dapat secara sewenang-wenang menghasilkan xatau 0, atau mungkin menjebak pada beberapa platform yang tidak jelas" ke " x>>32dapat menyebabkan kompiler untuk menulis ulang arti dari kode lain"? Bukti paling awal yang bisa saya temukan adalah dari 2009, tapi saya ingin tahu apakah ada bukti sebelumnya.
supercat

@Dupuplikator: Pengganti saya berfungsi dengan baik untuk nilai yang masuk akal, yaitu 0 <= jumlah <32. Dan apakah (nilai >> 32 - jumlah) Anda benar atau tidak, saya tidak peduli, tetapi mengabaikan tanda kurung seburuk bug.
gnasher729

Tidak melihat batasan untuk 0<=amount && amount<32. Apakah nilai yang lebih besar / lebih kecil masuk akal? Saya pikir apakah yang mereka lakukan adalah bagian dari pertanyaan. Dan tidak menggunakan tanda kurung dalam menghadapi bit-ops mungkin merupakan ide yang buruk, tentu, tetapi tentu saja bukan bug.
Deduplicator

@Deduplicator ini adalah karena beberapa CPU native menerapkan operator pergeseran sebagai (y mod 32)untuk 32-bit xdan (y mod 64)64-bit x. Perhatikan bahwa relatif mudah untuk mengeluarkan kode yang akan mencapai perilaku seragam di semua arsitektur CPU - dengan menutupi jumlah shift. Ini biasanya membutuhkan satu instruksi tambahan. Tapi sayang ...
rwong

-1

Ini karena ada bug dalam kode Anda:

uint32_t blah(uint32_t x, uint32_t y)
{
    if (y != 0) 
    {
        foo(y);
        return x; ////// missing return here //////
    }
    return rotateleft(x, y);
}

Dengan kata lain, itu hanya melompati penghalang kausalitas jika kompilator melihat bahwa, dengan input tertentu, Anda memohon perilaku tidak terdefinisi tanpa keraguan .

Dengan kembali tepat sebelum doa perilaku tidak terdefinisi, Anda memberi tahu kompiler bahwa Anda secara sadar mencegah perilaku tidak terdefinisi tersebut untuk dieksekusi, dan kompilator mengakui hal itu.

Dengan kata lain, ketika Anda memiliki kompiler yang mencoba untuk menegakkan spesifikasi dengan cara yang sangat ketat, Anda harus mengimplementasikan setiap validasi argumen yang mungkin dalam kode Anda. Selanjutnya, validasi ini harus terjadi sebelum doa dari perilaku yang tidak terdefinisi tersebut.

Tunggu! Dan masih ada lagi!

Sekarang, dengan kompiler melakukan hal-hal yang super-gila namun super-logis, Anda harus memberi tahu kompiler bahwa suatu fungsi tidak seharusnya melanjutkan eksekusi. Dengan demikian, noreturnkata kunci pada foo()fungsi sekarang menjadi wajib .


4
Maaf, tapi itu bukan pertanyaannya.
Deduplicator

@Deduplicator Seandainya bukan itu pertanyaannya, pertanyaan ini seharusnya ditutup karena berbasis pendapat (debat terbuka). Secara efektif itu adalah menebak motivasi dari komite C dan C ++ dan dari vendor / penulis kompiler.
rwong

1
Ini adalah pertanyaan tentang interpretasi historis, penggunaan dan alasan UB dalam bahasa C, tidak terbatas pada standar dan komite. Menebak komite-komite itu? Tidak begitu.
Deduplicator

1
... akui bahwa yang terakhir hanya boleh mengambil satu instruksi. Saya tidak tahu apakah hiper-modernis berusaha mati-matian untuk membuat C kompetitif di bidang di mana ia diganti, tetapi mereka merusak kegunaannya di satu-satunya bidang di mana itu adalah raja, dan berasal dari apa yang saya tahu membuat pengoptimalan yang berguna lebih sulit daripada lebih mudah.
supercat

1
@ago: Sebenarnya, pada sistem dengan 24 bit int yang pertama akan berhasil dan yang kedua akan gagal. Dalam sistem embeddeed, semuanya berjalan. Atau banyak yang berjalan; saya dapat mengingat satu di mana int = 32 bit, dan panjang = 40 bit yang merupakan satu-satunya sistem yang pernah saya lihat di mana lama jauh lebih efisien daripada int32_t.
gnasher729
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.