Apakah ada kasus cerdas modifikasi kode runtime?


119

Dapatkah Anda memikirkan penggunaan yang sah (cerdas) untuk modifikasi kode runtime (program yang memodifikasi kode itu sendiri saat runtime)?

Sistem operasi modern tampaknya tidak menyukai program yang melakukan ini karena teknik ini telah digunakan oleh virus untuk menghindari deteksi.

Yang dapat saya pikirkan adalah beberapa jenis pengoptimalan runtime yang akan menghapus atau menambahkan beberapa kode dengan mengetahui sesuatu pada saat runtime yang tidak dapat diketahui pada waktu kompilasi.


8
Pada arsitektur modern, ini sangat mengganggu caching dan alur instruksi: kode yang mengubah diri sendiri pada akhirnya tidak akan memodifikasi cache, jadi Anda akan memerlukan penghalang, dan ini kemungkinan akan membuat kode Anda lambat. Dan Anda tidak dapat mengubah kode yang sudah ada dalam pipeline instruksi. Jadi setiap pengoptimalan berdasarkan kode modifikasi diri harus dilakukan jauh sebelum kode dijalankan agar memiliki dampak kinerja yang lebih baik daripada, katakanlah, pemeriksaan waktu proses.
Alexandre C.

7
@Alexandre: itu umum untuk kode yang mengubah sendiri untuk membuat modifikasi jarang berbeda (misalnya sekali, dua kali) meskipun dieksekusi berkali-kali, sehingga biaya satu kali bisa tidak signifikan.
Tony Delroy

7
Tidak yakin mengapa ini diberi tag C atau C ++, karena keduanya tidak memiliki mekanisme untuk ini.
MSalters

4
@Alexandre: Microsoft Office dikenal melakukan hal itu. Akibatnya (?) Semua prosesor x86 memiliki dukungan yang sangat baik untuk mengubah kode sendiri. Pada prosesor lain diperlukan sinkronisasi yang mahal yang membuat semuanya kurang menarik.
Mackie Messer

3
@ Cawas: Biasanya perangkat lunak pembaruan otomatis akan mengunduh rakitan baru dan / atau berkas yang dapat dieksekusi dan menimpa yang sudah ada. Kemudian perangkat lunak akan dimulai ulang. Inilah yang dilakukan firefox, adobe, dll. Memodifikasi sendiri biasanya berarti bahwa selama kode waktu proses ditulis ulang dalam memori oleh aplikasi karena beberapa parameter dan tidak selalu disimpan kembali ke disk. Misalnya, ini mungkin mengoptimalkan seluruh jalur kode jika dapat dengan cerdas mendeteksi jalur tersebut tidak akan dijalankan selama proses khusus ini untuk mempercepat eksekusi.
NotMe

Jawaban:


117

Ada banyak kasus yang valid untuk modifikasi kode. Menghasilkan kode pada waktu proses dapat berguna untuk:

  • Beberapa mesin virtual menggunakan kompilasi JIT untuk meningkatkan kinerja.
  • Menghasilkan fungsi khusus dengan cepat telah lama menjadi hal biasa dalam grafik komputer. Lihat misalnya Rob Pike dan Bart Locanthi dan John Reiser Hardware Software Tradeoffs untuk Bitmap Graphics di Blit (1984) atau posting ini (2006) oleh Chris Lattner tentang penggunaan LLVM oleh Apple untuk spesialisasi kode runtime dalam tumpukan OpenGL mereka.
  • Dalam beberapa kasus, perangkat lunak menggunakan teknik yang dikenal sebagai trampolin yang melibatkan pembuatan kode secara dinamis pada tumpukan (atau tempat lain). Contohnya adalah fungsi bersarang GCC dan mekanisme sinyal dari beberapa Unix.

Terkadang kode diterjemahkan ke dalam kode saat runtime (ini disebut terjemahan biner dinamis ):

  • Emulator seperti Apple's Rosetta menggunakan teknik ini untuk mempercepat emulasi. Contoh lain adalah software morphing kode Transmeta .
  • Debugger dan profiler yang canggih seperti Valgrind atau Pin menggunakannya untuk melengkapi kode Anda saat sedang dijalankan.
  • Sebelum ekstensi dibuat ke set instruksi x86, perangkat lunak virtualisasi seperti VMWare tidak dapat langsung menjalankan kode x86 istimewa di dalam mesin virtual. Alih-alih itu harus menerjemahkan instruksi yang bermasalah dengan cepat menjadi kode khusus yang lebih sesuai.

Modifikasi kode dapat digunakan untuk mengatasi keterbatasan set instruksi:

  • Ada suatu masa (dahulu kala, saya tahu), ketika komputer tidak memiliki instruksi untuk kembali dari subrutin atau ke alamat memori secara tidak langsung. Kode modifikasi diri adalah satu-satunya cara untuk mengimplementasikan subrutin, pointer dan array .

Lebih banyak kasus modifikasi kode:

  • Banyak debugger mengganti instruksi untuk mengimplementasikan breakpoint .
  • Beberapa linker dinamis mengubah kode pada saat runtime. Artikel ini memberikan beberapa latar belakang tentang relokasi runtime DLL Windows, yang secara efektif merupakan bentuk modifikasi kode.

10
Daftar ini tampaknya mencampurkan contoh kode yang memodifikasi dirinya sendiri, dan kode yang memodifikasi kode lain, seperti linker.
AShelly

6
@ Ashelly: Nah, jika Anda menganggap penaut / pemuat dinamis sebagai bagian dari kode, maka ia memodifikasi dirinya sendiri. Mereka tinggal di ruang alamat yang sama, jadi menurut saya itu adalah sudut pandang yang valid.
Mackie Messer

1
Oke, daftar sekarang membedakan antara program dan perangkat lunak sistem. Saya harap ini masuk akal. Pada akhirnya, klasifikasi apa pun bisa diperdebatkan. Semuanya bermuara pada apa yang Anda masukkan ke dalam definisi program (atau kode).
Mackie Messer

35

Ini telah dilakukan dalam grafik komputer, khususnya perender perangkat lunak untuk tujuan pengoptimalan. Pada waktu proses, status banyak parameter diperiksa dan versi kode rasterizer yang dioptimalkan dihasilkan (berpotensi menghilangkan banyak persyaratan) yang memungkinkan seseorang untuk membuat grafik primitif misalnya segitiga jauh lebih cepat.


5
Bacaan yang menarik adalah artikel 3-Bagian Pixomatic Michael Abrash tentang DDJ: drdobbs.com/architecture-and-design/184405765 , drdobbs.com/184405807 , drdobbs.com/184405848 . Tautan kedua (Bagian2) berbicara tentang tukang las kode Pixomatic untuk pipa piksel.
typo.pl

1
Artikel yang sangat bagus tentang topik ini. Dari 1984, tapi masih bacaan yang bagus: Rob Pike dan Bart Locanthi dan John Reiser. Pengorbanan Perangkat Lunak Perangkat Keras untuk Bitmap Graphics di Blit .
Mackie Messer

5
Charles Petzold menjelaskan salah satu contoh semacam ini dalam buku berjudul "Beautiful Code": amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/…
Nawaz

3
Jawaban ini berbicara tentang menghasilkan kode, tetapi pertanyaannya adalah menanyakan tentang memodifikasi kode ...
Timwi

3
@Timwi - itu memang mengubah kode. Daripada menangani rantai besar jika itu mengurai bentuk sekali dan menulis ulang penyaji sehingga itu diatur untuk jenis bentuk yang benar tanpa harus memeriksa setiap saat. Menariknya ini sekarang umum dengan kode opencl - karena dikompilasi dengan cepat Anda dapat menulis ulang untuk kasus tertentu saat runtime
Martin Beckett

23

Salah satu alasan yang valid adalah karena set instruksi asm kekurangan beberapa instruksi yang diperlukan, yang dapat Anda buat sendiri. Contoh: Pada x86 tidak ada cara untuk membuat interupsi ke variabel dalam register (misalnya membuat interupsi dengan nomor interupsi di ax). Hanya nomor const yang dikodekan ke dalam opcode yang diizinkan. Dengan kode selfmodifying seseorang dapat meniru perilaku ini.


Cukup adil. Apakah ada gunanya teknik ini? Sepertinya berbahaya.
Alexandre C.

4
@Alexandre C .: Jika saya ingat banyak perpustakaan runtime (C, Pascal, ...) harus DOS kali fungsi untuk melakukan panggilan Interupsi. Dengan demikian, fungsi mendapatkan nomor interupsi sebagai parameter yang harus Anda suplai untuk fungsi tersebut (tentu saja jika jumlahnya konstan Anda dapat menghasilkan kode yang benar, tetapi itu tidak dijamin). Dan semua pustaka menerapkannya dengan kode modifikasi sendiri.
flolo

Anda dapat menggunakan kasus sakelar untuk melakukannya tanpa modifikasi kode. Penurunannya adalah bahwa kode keluaran akan lebih besar
phuclv

17

Beberapa kompiler menggunakannya untuk inisialisasi variabel statis, menghindari biaya bersyarat untuk akses selanjutnya. Dengan kata lain, mereka menerapkan "jalankan kode ini hanya sekali" dengan menimpa kode tersebut tanpa operasi saat pertama kali dijalankan.


1
Sangat bagus, terutama jika itu menghindari mutex locks / unlocks.
Tony Delroy

2
Betulkah? Bagaimana ini untuk kode berbasis ROM, atau untuk kode yang dieksekusi di segmen kode yang dilindungi dari penulisan?
Ira Baxter

1
@Ira Baxter: kompiler apa pun yang memancarkan kode yang dapat direlokasi tahu bahwa segmen kode dapat ditulisi, setidaknya selama startup. Jadi pernyataan "beberapa kompiler menggunakannya" masih memungkinkan.
MSalters

17

Ada banyak kasus:

  • Virus biasanya menggunakan kode modifikasi sendiri untuk "menyederhanakan" kode mereka sebelum dieksekusi, tetapi teknik tersebut juga dapat berguna dalam rekayasa balik yang membuat frustrasi, cracking, dan peretasan yang tidak diinginkan
  • Dalam beberapa kasus, mungkin ada titik tertentu selama runtime (misalnya segera setelah membaca file konfigurasi) ketika diketahui bahwa - selama sisa masa proses - cabang tertentu akan selalu atau tidak pernah diambil: daripada tidak perlu memeriksa beberapa variabel untuk menentukan jalan ke cabang, instruksi cabang itu sendiri dapat dimodifikasi sesuai
    • Misalnya, dapat diketahui bahwa hanya satu dari kemungkinan tipe turunan yang akan ditangani, sehingga pengiriman virtual dapat diganti dengan panggilan tertentu.
    • Setelah mendeteksi perangkat keras mana yang tersedia, penggunaan kode yang cocok mungkin di-hardcode
  • Kode yang tidak perlu dapat diganti dengan instruksi no-op atau lompatan di atasnya, atau bit kode berikutnya digeser langsung ke tempatnya (lebih mudah jika menggunakan opcode yang tidak tergantung posisi)
  • Kode yang ditulis untuk memfasilitasi debuggingnya sendiri mungkin memasukkan instruksi jebakan / sinyal / interupsi yang diharapkan oleh debugger di lokasi yang strategis.
  • Beberapa ekspresi predikat berdasarkan input pengguna mungkin dikompilasi menjadi kode asli oleh perpustakaan
  • Menyebariskan beberapa operasi sederhana yang tidak terlihat hingga runtime (misalnya dari pustaka yang dimuat secara dinamis) ...
  • Menambahkan langkah-langkah instrumentasi / pembuatan profil mandiri secara bersyarat
  • Crack dapat diimplementasikan sebagai pustaka yang mengubah kode yang memuatnya (bukan "sendiri" yang memodifikasi dengan tepat, tetapi membutuhkan teknik dan izin yang sama).
  • ...

Beberapa model keamanan OS berarti kode modifikasi sendiri tidak dapat berjalan tanpa hak akses root / admin, sehingga tidak praktis untuk penggunaan tujuan umum.

Dari Wikipedia:

Perangkat lunak aplikasi yang berjalan di bawah sistem operasi dengan keamanan W ^ X yang ketat tidak dapat menjalankan instruksi di halaman yang diizinkan untuk menulis — hanya sistem operasi itu sendiri yang diizinkan untuk menulis instruksi ke memori dan kemudian menjalankan instruksi tersebut.

Pada OS tersebut, bahkan program seperti Java VM memerlukan hak akses root / admin untuk menjalankan kode JIT mereka. (Lihat http://en.wikipedia.org/wiki/W%5EX untuk lebih jelasnya)


2
Anda tidak memerlukan hak root untuk mengubah kode sendiri. Begitu pula dengan Java VM.
Mackie Messer

Saya tidak tahu beberapa OS begitu ketat. Tapi tentu masuk akal di beberapa aplikasi. Namun saya bertanya-tanya apakah menjalankan Java dengan hak akses root benar-benar meningkatkan keamanan ...
Mackie Messer

@Mackie: Saya pikir itu harus menguranginya, tetapi mungkin itu dapat mengatur beberapa izin memori kemudian mengubah uid efektif kembali ke beberapa akun pengguna ...?
Tony Delroy

Ya, saya berharap mereka memiliki mekanisme terperinci untuk memberikan izin guna menyertai model keamanan yang ketat.
Mackie Messer

15

The Sintesis OS pada dasarnya dievaluasi sebagian program anda sehubungan dengan panggilan API, dan diganti kode OS dengan hasilnya. Manfaat utamanya adalah banyak pengecekan kesalahan hilang (karena jika program Anda tidak akan meminta OS untuk melakukan sesuatu yang bodoh, itu tidak perlu diperiksa).

Ya, itu adalah contoh pengoptimalan waktu proses.


Saya gagal untuk mengerti maksudnya. Jika mengatakan panggilan sistem akan dilarang oleh OS, Anda kemungkinan akan mendapatkan kesalahan kembali sehingga Anda harus memeriksa kodenya, bukan? Tampaknya bagi saya bahwa memodifikasi file yang dapat dieksekusi alih-alih mengembalikan kode kesalahan adalah semacam rekayasa berlebihan.
Alexandre C.

@Alexandre C.: Anda mungkin dapat menghilangkan pemeriksaan pointer nol dengan cara itu. Seringkali jelas bagi penelepon bahwa argumen itu valid.
MSalters

@Alexandre: Anda dapat membaca penelitian di tautan. Saya pikir mereka mendapatkan percepatan yang cukup mengesankan, dan itulah intinya: -}
Ira Baxter

2
Untuk sistem yang relatif sepele dan tidak terikat I / O, penghematannya signifikan. Misalnya, jika Anda menulis deamon untuk Unix, ada banyak syscall boiler-plate yang Anda lakukan untuk memutuskan stdio, menyiapkan berbagai penangan sinyal, dll. Jika Anda tahu bahwa parameter panggilan adalah konstanta, dan bahwa hasilnya akan selalu sama (menutup stdin, misalnya), banyak kode yang Anda jalankan dalam kasus umum tidak diperlukan.
Mark Bessey

1
Jika Anda membaca tesis, bab 8 berisi beberapa angka yang sangat mengesankan tentang I / O waktu nyata non-sepele untuk akuisisi data. Ingat bahwa ini adalah tesis pertengahan 1980-an, dan mesin yang dia jalankan berumur 10? Mhz 68000, dia mampu dalam perangkat lunak untuk menangkap data audio berkualitas CD (44.000 sampel per detik) dengan perangkat lunak lama biasa. Dia mengklaim bahwa stasiun kerja Sun (klasik Unix) hanya bisa mencapai sekitar 1/5 dari tingkat itu. Saya seorang pembuat kode bahasa assembly lama dari masa itu, dan ini cukup spektakuler.
Ira Baxter

9

Bertahun-tahun yang lalu saya menghabiskan pagi mencoba untuk men-debug beberapa kode modifikasi diri, satu instruksi mengubah alamat target dari instruksi berikut, yaitu, saya sedang menghitung alamat cabang. Itu ditulis dalam bahasa assembly dan bekerja dengan sempurna ketika saya mengikuti instruksi program satu per satu. Tetapi ketika saya menjalankan program itu gagal. Akhirnya, saya menyadari bahwa mesin sedang mengambil 2 instruksi dari memori dan (seperti instruksi yang diletakkan dalam memori) instruksi yang saya modifikasi telah diambil dan dengan demikian mesin sedang menjalankan versi instruksi yang tidak dimodifikasi (salah). Tentu saja, ketika saya men-debug, itu hanya melakukan satu instruksi dalam satu waktu.

Maksud saya, kode yang mengubah diri sendiri bisa sangat buruk untuk diuji / debug dan sering kali memiliki asumsi tersembunyi mengenai perilaku mesin (baik itu perangkat keras atau virtual). Selain itu, sistem tidak pernah dapat membagikan halaman kode di antara berbagai thread / proses yang dijalankan pada mesin multi-core (sekarang). Ini mengalahkan banyak manfaat untuk memori virtual, dll. Ini juga akan membatalkan pengoptimalan cabang yang dilakukan di tingkat perangkat keras.

(Catatan - saya tidak memasukkan JIT dalam kategori kode modifikasi diri. JIT menerjemahkan dari satu representasi kode ke representasi alternatif, itu tidak mengubah kode)

Secara keseluruhan, itu hanya ide yang buruk - sangat rapi, sangat tidak jelas, tetapi sangat buruk.

tentu saja - jika yang Anda miliki hanyalah memori 8080 dan ~ 512 byte, Anda mungkin harus menggunakan praktik semacam itu.


1
Saya tidak tahu, baik dan buruk sepertinya bukan kategori yang tepat untuk dipikirkan. Tentu Anda harus benar-benar tahu apa yang Anda lakukan dan juga mengapa Anda melakukannya. Tetapi programmer yang menulis kode itu mungkin tidak ingin Anda melihat apa yang dilakukan program itu. Tentu saja tidak enak jika harus men-debug kode seperti itu. Tetapi kode itu sangat mungkin dimaksudkan seperti itu.
Mackie Messer

CPU x86 modern memiliki deteksi SMC yang lebih kuat daripada yang diperlukan di atas kertas: Mengamati pengambilan instruksi lama pada x86 dengan kode yang dapat dimodifikasi sendiri . Dan pada sebagian besar CPU non-x86 (seperti ARM), cache instruksi tidak koheren dengan cache data sehingga diperlukan pembersihan / sinkronisasi manual sebelum byte yang baru disimpan dapat dieksekusi dengan andal sebagai instruksi. community.arm.com/processors/b/blog/posts/… . Bagaimanapun juga, kinerja SMC buruk pada CPU modern, kecuali jika Anda memodifikasinya sekali dan menjalankannya berkali-kali.
Peter Cordes

7

Dari tampilan kernel sistem operasi, setiap Just In Time Compiler dan Linker Runtime melakukan modifikasi diri teks program. Contoh yang menonjol adalah Penerjemah Skrip ECMA V8 Google.


5

Alasan lain untuk mengubah kode sendiri (sebenarnya kode "yang dihasilkan sendiri") adalah untuk menerapkan mekanisme kompilasi Just-In-time untuk kinerja. Misalnya, program yang membaca ekspresi algebric dan menghitungnya pada berbagai parameter input dapat mengubah ekspresi tersebut dalam kode mesin sebelum menyatakan kalkulasi.


5

Anda tahu kastanye lama bahwa tidak ada perbedaan logis antara perangkat keras dan perangkat lunak ... orang juga dapat mengatakan bahwa tidak ada perbedaan logis antara kode dan data.

Apa itu kode modifikasi sendiri? Kode yang menempatkan nilai-nilai dalam aliran eksekusi sehingga dapat ditafsirkan bukan sebagai data tetapi sebagai perintah. Tentu ada sudut pandang teoritis dalam bahasa fungsional bahwa sebenarnya tidak ada perbedaan. Saya mengatakan bahwa e dapat melakukan ini secara langsung dalam bahasa imperatif dan penyusun / penafsir tanpa asumsi status yang sama.

Apa yang saya maksud adalah dalam arti praktis bahwa data dapat mengubah jalur eksekusi program (dalam beberapa hal ini sangat jelas). Saya memikirkan sesuatu seperti compiler-compiler yang membuat tabel (array data) yang dilintasi seseorang dalam parsing, berpindah dari satu negara bagian ke negara bagian (dan juga memodifikasi variabel lain), seperti bagaimana sebuah program bergerak dari perintah ke perintah , memodifikasi variabel dalam prosesnya.

Jadi, bahkan dalam contoh biasa di mana kompilator membuat ruang kode dan merujuk ke ruang data yang sepenuhnya terpisah (heap), seseorang masih dapat memodifikasi data untuk mengubah jalur eksekusi secara eksplisit.


4
Tidak ada perbedaan logis, benar. Belum melihat terlalu banyak sirkuit terintegrasi yang dapat memodifikasi sendiri.
Ira Baxter

@ Mitch, IMO mengubah jalur exec tidak ada hubungannya dengan modifikasi kode (sendiri). Selain itu, Anda mengacaukan data dengan info. Saya tidak bisa menjawab komentar tahun untuk balasan saya di LSE b / c Saya dilarang di sana, sejak Feabruari, selama 3 tahun (1.000 hari) karena menyatakan dalam meta-LSE saya bahwa orang Amerika dan Inggris tidak memiliki bahasa Inggris.
Gennady Vanin Геннадий Ванин

4

Saya telah menerapkan program menggunakan evolusi untuk membuat algoritme terbaik. Ini menggunakan kode modifikasi diri untuk memodifikasi cetak biru DNA.


2

Salah satu kasus penggunaan adalah file tes EICAR yang merupakan file COM executable DOS yang sah untuk menguji program antivirus.

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Itu harus menggunakan modifikasi kode sendiri karena file yang dapat dieksekusi harus berisi hanya karakter ASCII yang dapat dicetak / dapat diketik dalam rentang [21h-60h, 7Bh-7Dh] yang membatasi jumlah instruksi yang dapat dienkode secara signifikan

Detailnya dijelaskan di sini


Ini juga digunakan untuk operasi floating-point di DOS

Beberapa kompiler akan memancarkan CD xxdengan xx mulai dari 0x34-0x3B di tempat instruksi floating-point x87. Karena CDadalah opcode untuk intinstruksi, itu akan melompat ke interupsi 34h-3Bh dan meniru instruksi itu dalam perangkat lunak jika koprosesor x87 tidak tersedia. Jika tidak, penangan interupsi akan mengganti 2 byte tersebut dengan 9B Dxsehingga eksekusi selanjutnya akan ditangani secara langsung oleh x87 tanpa emulasi.

Apa protokol untuk emulasi floating point x87 di MS-DOS?


1

The Linux Kernel memiliki loadable Kernel Modul yang melakukan hal itu.

Emacs juga memiliki kemampuan ini dan saya menggunakannya setiap saat.

Apa pun yang mendukung arsitektur plugin dinamis pada dasarnya memodifikasi kodenya saat runtime.


4
hampir tidak. memiliki pustaka yang dapat dimuat secara dinamis yang tidak selalu ada tidak ada hubungannya dengan kode yang mengubah diri sendiri.
Dov

1

Saya menjalankan analisis statistik terhadap database yang terus diperbarui. Model statistik saya ditulis dan ditulis ulang setiap kali kode dijalankan untuk mengakomodasi data baru yang tersedia.


0

Skenario di mana ini dapat digunakan adalah program pembelajaran. Menanggapi masukan pengguna, program mempelajari algoritme baru:

  1. itu mencari basis kode yang ada untuk algoritma serupa
  2. jika tidak ada algoritma serupa di basis kode, program hanya menambahkan algoritma baru
  3. Jika ada algoritma serupa, program (mungkin dengan bantuan dari pengguna) memodifikasi algoritma yang ada agar dapat melayani tujuan lama dan tujuan baru

Ada pertanyaan bagaimana melakukan itu di Java: Apa saja kemungkinan modifikasi diri kode Java?


-1

Versi terbaik dari ini mungkin Lisp Macros. Tidak seperti makro C yang hanya sebuah preprocessor Lisp memungkinkan Anda memiliki akses ke seluruh bahasa pemrograman setiap saat. Ini tentang fitur paling kuat di cadel dan tidak ada dalam bahasa lain.

Saya sama sekali bukan seorang ahli tetapi salah satu dari orang-orang cadel membicarakannya! Ada alasan mengapa mereka mengatakan bahwa Lisp adalah bahasa yang paling kuat di sekitar dan orang pintar tidak karena mereka mungkin benar.


2
Apakah itu benar-benar membuat kode modifikasi sendiri atau hanya preprocessor yang lebih kuat (yang akan menghasilkan fungsi)?
Brendan Long

@Brendan: memang, tapi itu adalah cara yang tepat untuk melakukan preprocessing. Tidak ada modifikasi kode runtime di sini.
Alexandre C.
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.