C ++: Licik dan Cinta, atau Apa yang Bisa Salah?





“C memudahkan untuk menembak diri sendiri di kaki. Lebih sulit untuk melakukan ini dalam C ++, tetapi itu akan memakan banyak waktu. ”- Björn Stroustrup, pencipta C ++.


Dalam artikel ini, kami akan menunjukkan kepada Anda cara menulis kode yang stabil, aman, dan andal, dan betapa mudahnya untuk benar-benar memecahnya secara tidak sengaja. Untuk ini, kami mencoba mengumpulkan materi yang paling berguna dan menarik.







Di SimbirSoft, kami bekerja sama dengan proyek Secure Code Warrior untuk melatih pengembang lain untuk menciptakan solusi yang aman. Khusus untuk Habr, kami menerjemahkan artikel yang ditulis oleh penulis kami untuk portal CodeProject.com.



Jadi untuk kodenya!



Berikut adalah potongan kecil kode C ++ abstrak. Kode ini secara khusus ditulis untuk mendemonstrasikan semua jenis masalah dan kerentanan yang berpotensi dapat ditemukan pada proyek yang sangat nyata. Seperti yang Anda lihat, ini adalah kode DLL Windows (ini adalah poin penting). Misalkan seseorang akan menggunakan kode ini dalam beberapa solusi (aman, tentu saja).



Perhatikan kodenya. Menurut Anda, apa yang bisa salah?



Kode
class Finalizer
{
    struct Data
    {
        int i = 0;
        char* c = nullptr;
        
        union U
        {
            long double d;
            
            int i[sizeof(d) / sizeof(int)];
            
            char c [sizeof(i)];
        } u = {};
        
        time_t time;
    };
    
    struct DataNew;
    DataNew* data2 = nullptr;
    
    typedef DataNew* (*SpawnDataNewFunc)();
    SpawnDataNewFunc spawnDataNewFunc = nullptr;
    
    typedef Data* (*Func)();
    Func func = nullptr;
    
    Finalizer()
    {
        func = GetProcAddress(OTHER_LIB, "func")
        
        auto data = func();
        
        auto str = data->c;
        
        memset(str, 0, sizeof(str));
        
        data->u.d = 123456.789;
        
        const int i0 = data->u.i[sizeof(long double) - 1U];
        
        spawnDataNewFunc = GetProcAddress(OTHER_LIB, "SpawnDataNewFunc")
        data2 = spawnDataNewFunc();
    }
    
    ~Finalizer()
    {
        auto data = func();
        
        delete[] data2;
    }
};

Finalizer FINALIZER;

HMODULE OTHER_LIB;
std::vector<int>* INTEGERS;

DWORD WINAPI Init(LPVOID lpParam)
{
    OleInitialize(nullptr);
    
    ExitThread(0U);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    static std::vector<std::thread::id> THREADS;
    
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            CoInitializeEx(nullptr, COINIT_MULTITHREADED);
            
            srand(time(nullptr));
            
            OTHER_LIB = LoadLibrary("B.dll");
            
            if (OTHER_LIB = nullptr)
                return FALSE;
            
            CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);
        break;
        
        case DLL_PROCESS_DETACH:
            CoUninitialize();
            
            OleUninitialize();
            {
                free(INTEGERS);
                
                const BOOL result = FreeLibrary(OTHER_LIB);
                
                if (!result)
                    throw new std::runtime_error("Required module was not loaded");
                
                return result;
            }
        break;
        
        case DLL_THREAD_ATTACH:
            THREADS.push_back(std::this_thread::get_id());
        break;
        
        case DLL_THREAD_DETACH:
            THREADS.pop_back();
        break;
    }
    return TRUE;
}

__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()
{
    for (int i : integers)
        i *= c;
    
    INTEGERS = new std::vector<int>(integers);
}

int Random()
{
    return rand() + rand();
}

__declspec(dllexport) long long int __cdecl _GetInt(int a)
{
    return 100 / a <= 0 ? a : a + 1 + Random();
}




Mungkin Anda menganggap kode ini sederhana, jelas, dan cukup aman? Atau mungkin Anda menemukan beberapa masalah di dalamnya? Mungkin bahkan selusin atau dua?



Sebenarnya, ada lebih dari 43 potensi ancaman dengan berbagai tingkat signifikansi dalam cuplikan ini !







Apa yang harus Anda perhatikan



1) sizeof (d) (di mana d adalah double panjang) belum tentu kelipatan sizeof (int)



int i[sizeof(d) / sizeof(int)];


Situasi ini tidak diuji atau ditangani di sini. Misalnya, panjang ganda mungkin 10 byte pada beberapa platform (yang tidak benar untuk kompiler MS VS , tetapi benar untuk RAD Studio , sebelumnya dikenal sebagai C ++ Builder ).



int juga dapat berukuran berbeda tergantung pada platformnya (kode di atas untuk Windows , oleh karena itu, dalam kaitannya dengan situasi khusus ini, masalahnya agak dibuat-buat, tetapi untuk kode portabel masalah ini sangat relevan).



Semua ini bisa menjadi masalah jika kita ingin menggunakan apa yang disebut mengetik pun . Ngomong-ngomong, itu menyebabkan perilaku yang tidak terdefinisisesuai dengan standar bahasa C ++. Namun, merupakan praktik umum untuk menggunakan permainan kata-kata pengetikan , karena kompiler modern biasanya mendefinisikan perilaku yang diharapkan yang benar untuk kasus tertentu (seperti, misalnya, GCC ).







Sumber: Medium.com



Ngomong-ngomong, tidak seperti C ++, di C modern, pelesetan pengetikan dapat diterima dengan sempurna (Anda tahu bahwa C ++ dan C adalah bahasa yang berbeda , dan Anda tidak boleh berharap mengetahui C jika Anda tahu C ++, dan sebaliknya, kan?)



Solusi: gunakan static_assertuntuk mengontrol semua asumsi tersebut pada waktu kompilasi. Ini akan memperingatkan Anda jika ada yang salah dengan ukuran jenis:



static_assert(0U == (sizeof(d) % sizeof(int)), “Houston, we have a problem”);


2) time_t adalah makro, di Visual Studio dapat merujuk ke tipe integer 32-bit (lama) atau 64-bit (baru)



time_t time;


Akses ke variabel jenis ini dari modul yang dapat dieksekusi yang berbeda (misalnya, file yang dapat dieksekusi dan DLL yang dimuatnya) dapat menyebabkan baca / tulis di luar batasan objek jika dua biner dikompilasi dengan representasi fisik yang berbeda dari jenis ini. Yang, pada gilirannya, akan menyebabkan kerusakan memori atau pembacaan sampah.







Solusi: pastikan bahwa jenis yang sama dari ukuran yang ditentukan secara ketat digunakan untuk pertukaran data antara semua modul:



int64_t time;


3) B.dll (pegangan yang disimpan oleh OTHER_LIB variabel ) telah tidak belum pernah dimuat pada saat ketika kita mengakses variabel di atas, jadi kami tidak bisa mendapatkan alamat dari fungsi perpustakaan ini



4) masalah dengan urutan inisialisasi objek statis ( SIOF ): ( OTHER_LIB objek digunakan dalam kode sebelum diinisialisasi)



func = GetProcAddress(OTHER_LIB, "func");


FINALIZER adalah objek statis yang dibuat sebelum memanggil fungsi DllMain . Dalam konstruktornya, kami mencoba menggunakan pustaka yang belum dimuat. Masalahnya diperparah oleh fakta bahwa OTHER_LIB statis yang digunakan oleh FINALIZER statis ditempatkan di unit terjemahan di hilir. Ini berarti itu juga akan diinisialisasi (nol) nanti. Artinya, pada saat akan diakses, itu akan berisi beberapa sampah pseudo-random . WinAPIsecara umum, ia harus bereaksi normal terhadap hal ini, karena dengan tingkat probabilitas yang tinggi tidak akan ada modul yang dimuat dengan deskriptor seperti itu sama sekali. Dan bahkan jika ada kebetulan yang benar-benar luar biasa dan itu akan tetap terjadi - kecil kemungkinannya akan memiliki fungsi bernama "Func" .



Solusi: Saran umum adalah menghindari penggunaan objek global, terutama yang kompleks, terutama jika mereka bergantung satu sama lain, terutama di DLL . Namun, jika Anda masih membutuhkannya karena alasan apa pun, berhati-hatilah dan berhati-hati dengan urutan inisialisasi. Untuk mengontrol urutan ini , letakkan semua contoh (definisi) objek global ke dalam satu unit terjemahandalam urutan yang benar untuk memastikan mereka diinisialisasi dengan benar.



5) hasil yang dikembalikan sebelumnya tidak diperiksa sebelum digunakan



auto data = func();


func adalah penunjuk fungsi . Dan itu harus menunjuk ke fungsi dari B.dll . Namun, karena kami benar-benar gagal semuanya di langkah sebelumnya, ini akan menjadi nullptr . Jadi, mencoba untuk membedakannya, alih-alih panggilan fungsi yang diharapkan, kami mendapatkan pelanggaran akses atau kesalahan perlindungan umum atau sesuatu seperti itu.



Solusi: saat bekerja dengan kode eksternal (dalam kasus kami dengan WinAPI ), selalu periksa hasil kembalian dari fungsi yang dipanggil. Untuk sistem yang andal dan toleran terhadap kesalahan, aturan ini berlaku bahkan untuk fungsi yang memiliki kontrak ketat [tentang apa yang harus dikembalikan dan kapan].



6) membaca / menulis sampah saat bertukar data antar modul yang disusun dengan pengaturan alignment / padding yang berbeda



auto str = data->c;


Jika data struktur (yang digunakan untuk bertukar informasi antara modul komunikasi) memiliki modul-modul yang sama dalam presentasi fisik yang berbeda akan menghasilkan semua yang disebutkan sebelumnya pelanggaran akses , perlindungan memori kesalahan , segmentasi kesalahan , korupsi tumpukan , dll Atau kita akan membaca sampah saja. Hasil pastinya akan bergantung pada skenario penggunaan sebenarnya untuk memori ini. Semua ini dapat terjadi karena tidak ada pengaturan pelurusan / padding eksplisit untuk struktur itu sendiri . Oleh karena itu, jika pengaturan global ini pada saat kompilasi berbeda untuk modul yang berinteraksi, kita akan mengalami masalah.







Keputusan:pastikan bahwa semua struktur data bersama memiliki representasi fisik yang kuat, didefinisikan secara eksplisit dan jelas (menggunakan tipe ukuran tetap, penyelarasan yang ditentukan secara eksplisit, dll.) dan / atau biner interoperable telah dikompilasi dengan pengaturan penyelarasan global yang sama / isi.



Lihat juga
Alignment (C++ Declarations)

Data structure alignment

Struct padding in C++



7) menggunakan ukuran penunjuk ke larik alih-alih ukuran larik itu sendiri



memset(str, 0, sizeof(str));


Ini biasanya akibat kesalahan ketik yang sepele. Tetapi masalah ini juga berpotensi muncul ketika berhadapan dengan polimorfisme statis atau ketika kata kunci otomatis digunakan secara sembarangan ( terutama bila jelas digunakan secara berlebihan ). Namun, orang ingin berharap bahwa kompiler modern cukup pintar untuk mendeteksi masalah seperti itu pada waktu kompilasi, menggunakan kemampuan penganalisis statis internal .



Keputusan:



  • tidak pernah membingungkan sizeof ( <full object type> ) dan sizeof ( <object pointer type> );
  • jangan abaikan peringatan kompiler ;

  • Anda juga bisa menggunakan sedikit sihir boilerplate C ++ dengan menggabungkan typeid, constexpr, dan static_assert untuk memastikan bahwa tipe sudah benar pada waktu kompilasi ( ciri tipe juga dapat berguna di sini , khususnya std :: is_pointer ).


8) perilaku tidak terdefinisi saat mencoba membaca bidang gabungan yang berbeda dari yang sebelumnya digunakan untuk menetapkan nilai



9) dimungkinkan untuk mencoba membaca area memori yang valid jika ukuran ganda panjang berbeda antara modul biner



const int i0 = data->u.i[sizeof(long double) - 1U];


Ini sudah disebutkan sebelumnya, jadi di sini kita baru saja mendapat titik kehadiran lain dari masalah yang disebutkan sebelumnya.



Solusi: Jangan merujuk ke kolom selain yang Anda setel sebelumnya kecuali Anda yakin kompiler Anda menanganinya dengan benar. Pastikan ukuran tipe objek bersama sama di semua modul yang berinteraksi.



Lihat juga
Type-punning and strict-aliasing

What is the Strict Aliasing Rule and Why do we care?



10) bahkan jika B.dll dimuat dengan benar dan fungsi "func" telah diekspor dan diimpor dengan benar, B.dll masih dikeluarkan dari memori saat ini (karena fungsi sistem FreeLibrary sebelumnya dipanggil di bagian DLL_PROCESS_DETACH dari fungsi panggilan balik DllMain )



auto data = func();


Memanggil fungsi virtual pada objek jenis polimorfik yang sebelumnya dihancurkan, serta memanggil fungsi di pustaka dinamis yang sudah dibongkar, mungkin akan menghasilkan kesalahan panggilan virtual murni .



Solusi: Terapkan prosedur finalisasi yang benar dalam aplikasi untuk memastikan bahwa semua DLL telah selesai / dibongkar dalam urutan yang benar. Hindari menggunakan objek statis dengan logika kompleks di DL L. Hindari melakukan operasi apa pun di dalam pustaka setelah memanggil DllMain / DLL_PROCESS_DETACH (saat pustaka memasuki tahap terakhir dari siklus hidupnya - fase penghancuran objek statisnya).



Anda perlu memahami apa siklus hidup DLL:



) LoadLibrary



  • ( , )
  • DllMain -> DLL_PROCESS_ATTACH ( , )
  • [] DllMain -> DLL_THREAD_ATTACH / DLL_THREAD_DETACH ( , . 30).
  • , , (, ),
  • ( / , , )
  • , ()
  • ( / , , )
  • - : ,


) FreeLibrary



  • DllMain -> DLL_PROCESS_DETACH ( , )
  • ( , )






11) penghapusan penunjuk buram ( kompilator perlu mengetahui tipe lengkap untuk memanggil destruktor, jadi menghapus objek menggunakan penunjuk buram dapat menyebabkan kebocoran memori dan masalah lain)



12) jika destruktor DataNew bersifat virtual, bahkan jika kelas diekspor dan diimpor dengan benar dan lengkap informasi tentang itu, bagaimanapun memanggil destruktornya pada tahap ini adalah masalah - ini mungkin akan mengarah pada panggilan fungsi virtual murni (karena tipe DataNew diimpor dari file B.dll yang sudah dibongkar ). Masalah ini mungkin terjadi meskipun destruktornya bukan virtual.



13) jika kelas DataNew adalah tipe polimorfik abstrak, dan kelas dasarnya memiliki penghancur virtual murni tanpa badan, dalam hal apa pun pemanggilan fungsi virtual murni akan terjadi .



14) perilaku tidak terdefinisi jika memori dialokasikan menggunakan new dan dihapus menggunakan delete []



delete[] data2;


Secara umum, Anda harus selalu berhati - hati saat membebaskan objek yang diterima dari modul eksternal.



Ini juga merupakan praktik yang baik untuk membidik petunjuk ke objek yang hancur.



Keputusan:



  • saat menghapus sebuah objek, tipe lengkapnya harus diketahui
  • semua perusak harus memiliki tubuh
  • perpustakaan tempat kode diekspor tidak boleh dibongkar terlalu dini
  • selalu gunakan berbagai bentuk baru dan hapus dengan benar, jangan bingung
  • penunjuk ke objek jarak jauh harus menjadi nol.






Perhatikan juga hal berikut:

- memanggil delete pada pointer ke void akan menghasilkan perilaku tidak terdefinisi,

fungsi virtual murni tidak boleh dipanggil dari konstruktor

- memanggil fungsi virtual dalam konstruktor tidak akan virtual

- mencoba untuk menghindari manajemen memori manual - menggunakan container , memindahkan semantik, dan petunjuk cerdas



Lihat juga
Heap corruption: What could the cause be?



15) ExitThread adalah metode yang disukai untuk keluar dari utas di C. Dalam C ++, ketika fungsi ini dipanggil, utas akan berhenti sebelum memanggil destruktor objek lokal (dan pembersihan otomatis lainnya), jadi menghentikan utas di C ++ harus dilakukan hanya dengan kembali dari fungsi utas



ExitThread(0U);


Solusi: Jangan pernah menggunakan fungsi ini secara manual dalam kode C ++.



16) di badan DllMain, memanggil fungsi standar apa pun yang memerlukan DLL sistem selain Kernel32.dll dapat menyebabkan berbagai masalah yang sulit didiagnosis



CoInitializeEx(nullptr, COINIT_MULTITHREADED);


Solusi di DllMain:



  • hindari inisialisasi (de) yang rumit
  • hindari memanggil fungsi dari perpustakaan lain (atau setidaknya berhati-hatilah dengan ini)






17) inisialisasi yang salah dari generator bilangan pseudo-random dalam lingkungan multithreaded



18) karena waktu yang dikembalikan oleh fungsi waktu memiliki resolusi 1 detik., Setiap utas dalam program yang memanggil fungsi ini selama periode waktu ini akan menerima nilai yang sama pada keluaran. Menggunakan nomor ini untuk menginisialisasi PRNG dapat menyebabkan tabrakan (misalnya, pembuatan nama pseudo-random yang sama untuk file sementara, nomor port yang sama, dll.). Salah satu solusi yang mungkin adalah mencampur ( xor ) hasil yang dihasilkan dengan beberapa nilai pseudo-random , seperti alamat tumpukan atau objek apa pun di heap, waktu yang lebih akurat, dll.



srand(time(nullptr));


Solusi: MS VS membutuhkan inisialisasi PRNG untuk setiap utas . Selain itu, menggunakan waktu Unix sebagai penginisialisasi memberikan entropi yang tidak mencukupi , pembangkitan nilai inisialisasi yang lebih canggih lebih disukai .



Lihat juga
Is there an alternative to using time to seed a random number generation?

C++ seeding surprises

Getting random numbers in a thread-safe way [C#]


19) mungkin buntu atau crash (atau membuat loop ketergantungan dalam urutan pemuatan DLL )



OTHER_LIB = LoadLibrary("B.dll");


Solusi: Jangan gunakan LoadLibrary di titik masuk DllMain . Setiap inisialisasi kompleks (de) harus dilakukan dalam fungsi yang diekspor pengembang DLL tertentu seperti "Init" dan "Deint" . Perpustakaan menyediakan fungsi-fungsi ini kepada pengguna, dan pengguna harus memanggilnya dengan benar pada waktu yang tepat. Kedua belah pihak harus benar-benar mematuhi kontrak ini.







20) kesalahan ketik (kondisi selalu salah), logika program salah dan kemungkinan kebocoran sumber daya (karena OTHER_LIB tidak pernah diturunkan pada unduhan yang berhasil)



if (OTHER_LIB = nullptr)
    return FALSE;


Operator penugasan dengan menyalin mengembalikan tautan tipe kiri, mis. if akan memeriksa nilai OTHER_LIB (yang akan menjadi nullptr) dan nullptr akan ditafsirkan sebagai salah.



Solusi: Selalu gunakan bentuk terbalik untuk menghindari kesalahan ketik seperti ini:



if/while (<constant> == <variable/expression>)


21) disarankan untuk menggunakan fungsi sistem _beginthread untuk membuat utas baru dalam aplikasi (terutama jika aplikasi ditautkan dengan versi statis pustaka runtime C) jika tidak kebocoran memori dapat terjadi saat memanggil ExitThread, DisableThreadLibraryCalls



22) semua panggilan eksternal ke DllMain diserialisasi, jadi di bagian isi Fungsi ini tidak boleh mencoba membuat utas / proses atau berinteraksi dengannya, jika tidak, jalan buntu dapat terjadi



CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);


23) memanggil fungsi COM selama penghentian DLL dapat menyebabkan akses memori yang salah, karena komponen terkait mungkin sudah dibongkar



CoUninitialize();


24) tidak ada cara untuk mengontrol urutan bongkar muat layanan COM / OLE dalam proses, jadi jangan panggil OleInitialize atau OleUninitialize dari fungsi DllMain



OleUninitialize();


Lihat juga
COM Clients and Servers

In-process, Out-of-process, and Remote Servers



25) panggilan gratis untuk blok memori yang dialokasikan dengan



26 baru ) jika proses aplikasi sedang dalam proses menghentikan pekerjaannya (seperti yang ditunjukkan oleh nilai bukan nol dari parameter lpvReserved), semua utas dalam proses, kecuali yang saat ini, telah dihentikan atau dihentikan secara paksa ketika memanggil fungsi ExitProcess, yang dapat membuat beberapa sumber daya proses, seperti heap, dalam keadaan tidak konsisten. Akibatnya, DLL tidak aman untuk membersihkan sumber daya . Sebaliknya, DLL harus memungkinkan sistem operasi untuk mendapatkan kembali memori.



free(INTEGERS);


Solusi: Pastikan gaya C lama alokasi memori manual tidak dicampur dengan gaya C ++ "baru". Berhati-hatilah saat mengelola sumber daya dalam fungsi DllMain .



27) dapat menyebabkan DLL digunakan bahkan setelah sistem menjalankan kode keluarnya



const BOOL result = FreeLibrary(OTHER_LIB);


Solusi: Jangan panggil FreeLibrary di titik masuk DllMain.



28) utas saat ini (mungkin utama) akan macet



throw new std::runtime_error("    ");


Solusi: Hindari membuang pengecualian dalam fungsi DllMain. Jika DLL tidak dapat dimuat dengan benar karena alasan apa pun, fungsi tersebut seharusnya mengembalikan FALSE. Anda juga tidak boleh membuang pengecualian dari bagian DLL_PROCESS_DETACH.



Selalu berhati-hati saat melempar pengecualian di luar DLL. Objek kompleks apa pun (misalnya, kelas pustaka standar ) dapat memiliki representasi fisik yang berbeda (dan bahkan logika pekerjaan) dalam modul yang dapat dijalankan yang berbeda jika dikompilasi dengan versi pustaka waktu proses yang berbeda (tidak kompatibel) .







Cobalah untuk bertukar hanya tipe data sederhana antar modul(dengan ukuran tetap dan representasi biner yang terdefinisi dengan baik).



Ingatlah bahwa menghentikan utas utama akan secara otomatis menghentikan semua utas lainnya (yang tidak diakhiri dengan benar dan oleh karena itu dapat merusak memori, meninggalkan sinkronisasi primitif dan objek lain dalam keadaan yang tidak dapat diprediksi dan salah. Selain itu, utas ini sudah akan berhenti ada pada saat objek statis akan memulai dekonstruksi mereka sendiri, jadi jangan mencoba menunggu utas apa pun selesai di destruktor objek statis).



Lihat juga
Top 20 C++ multithreading mistakes and how to avoid them



29) Anda bisa melempar pengecualian (misalnya, std :: bad_alloc), yang tidak tertangkap di sini



THREADS.push_back(std::this_thread::get_id());


Karena bagian DLL_THREAD_ATTACH dipanggil dari beberapa kode eksternal yang tidak diketahui, jangan berharap untuk melihat perilaku yang benar di sini.



Solusi: Gunakan coba / tangkap untuk menyertakan pernyataan yang mungkin memunculkan pengecualian yang kemungkinan besar tidak dapat ditangani dengan benar (terutama jika keluar dari DLL ).



Lihat juga
How can I handle a destructor that fails?



30) UB jika aliran disajikan sebelum memuat DLL ini



THREADS.pop_back();


Utas yang sudah ada pada saat DLL dimuat (termasuk yang secara langsung memuat DLL ) tidak memanggil fungsi titik masuk dari DLL yang sedang dimuat (oleh karena itu mereka tidak terdaftar di vektor THREADS selama acara DLL_THREAD_ATTACH), sementara mereka masih memanggilnya dengan acara DLL_THREAD_DETACH setelah selesai.

Ini berarti bahwa jumlah panggilan ke bagian DLL_THREAD_ATTACH dan DLL_THREAD_DETACH dari fungsi DllMain akan berbeda.



31) lebih baik menggunakan tipe bilangan bulat berukuran tetap



32) melewatkan objek kompleks antar modul mungkin macet jika dikompilasi dengan tautan berbeda dan pengaturan kompilasi dan bendera (versi runtime library yang berbeda, dll.)



33) mengakses objek c dengan alamat virtualnya (yang digunakan bersama modul) dapat menyebabkan masalah jika pointer ditangani secara berbeda dalam modul ini (misalnya, jika modul dikaitkan dengan parameter LARGEADDRESSAWARE yang berbeda )



__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()


Lihat juga
Is it possible to use more than 2 Gbytes of memory in a 32-bit program launched in the 64-bit Windows?

Application with LARGEADDRESSAWARE flag set getting less virtual memory

Drawbacks of using /LARGEADDRESSAWARE for 32 bit Windows executables?

how to check if exe is set as LARGEADDRESSAWARE [C#]

/LARGEADDRESSAWARE [Ru]

ASLR (Address Space Layout Randomization) [Ru]



Dan...
Virtual memory

Physical Address Extension

Tagged pointer

std::ptrdiff_t

What is uintptr_t data type

Pointer arithmetic

Pointer aliasing

What is the strict aliasing rule?

reinterpret_cast conversion

restrict type qualifier



Daftar di atas hampir tidak lengkap, jadi Anda mungkin dapat menambahkan sesuatu yang penting di komentar.



Bekerja dengan pointer sebenarnya jauh lebih kompleks daripada yang biasanya orang pikirkan. Tanpa ragu, pengembang berpengalaman akan dapat mengingat nuansa dan kehalusan lain yang ada (misalnya, sesuatu tentang perbedaan antara penunjuk ke suatu objek dan penunjuk ke suatu fungsi , karena itu, mungkin, tidak semua bit penunjuk dapat digunakan , dll. .).







34) pengecualian dapat dilemparkan ke dalam fungsi :



INTEGERS = new std::vector<int>(integers);


spesifikasi throw () dari fungsi ini kosong:



__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()


std :: terduga dipanggil oleh runtime C ++ ketika spesifikasi pengecualian dilanggar: pengecualian dilempar dari fungsi yang spesifikasi pengecualiannya melarang pengecualian jenis ini.



Solusi: gunakan try / catch (terutama ketika mengalokasikan sumber daya, terutama di DLL ) atau bentuk nothrow dari operator baru. Bagaimanapun, jangan pernah membuat asumsi naif bahwa semua upaya untuk mengalokasikan berbagai jenis sumber daya akan selalu berakhir dengan sukses .



Lihat juga
RAII

We do not use C++ exceptions

Memory Limits for Windows and Windows Server Releases









Masalah 1: pembentukan nilai "lebih acak" seperti itu tidak benar. Menurut teorema batas pusat , jumlah variabel acak independen cenderung berdistribusi normal , dan tidak berdistribusi seragam (meskipun nilai aslinya sendiri terdistribusi secara seragam).



Masalah 2: Kemungkinan melimpah jenis bilangan bulat (yang merupakan perilaku tidak ditentukan untuk jenis bilangan bulat bertanda )



return rand() + rand();


Saat bekerja dengan generator bilangan acak semu, enkripsi, dan sejenisnya, selalu berhati-hatilah dalam menggunakan "solusi" buatan sendiri. Kecuali Anda memiliki pendidikan dan pengalaman khusus di bidang yang sangat spesifik ini, kemungkinan besar Anda akan mengakali diri sendiri dan membuat situasi menjadi lebih buruk.



35) nama fungsi yang diekspor akan didekorasi (diubah) untuk mencegah penggunaan "C"



36 eksternal ini ) nama yang dimulai dengan '_' secara implisit dilarang untuk C ++, karena gaya penamaan ini disediakan untuk STL



__declspec(dllexport) long long int __cdecl _GetInt(int a)


Beberapa masalah (dan kemungkinan solusinya):



37) rand tidak aman untuk thread, gunakan rand_r / rand_s sebagai gantinya



38) Rand sudah usang, lebih baik gunakan modern
C++11 <random>


39) bukanlah fakta bahwa fungsi rand diinisialisasi secara khusus untuk utas saat ini (MS VS memerlukan inisialisasi fungsi ini untuk setiap utas yang akan dipanggil)



40) ada generator khusus bilangan pseudo-acak , dan lebih baik menggunakannya dalam solusi tahan retasan (cocok solusi portabel seperti Libsodium / randombytes_buf , OpenSSL / RAND_bytes , dll.)



41) pembagian potensial dengan nol: dapat menyebabkan utas saat ini macet



42) operator dengan prioritas berbeda digunakan di baris yang sama , yang menyebabkan kekacauan dalam urutan perhitungan - gunakan tanda kurung dan / atau poin urutanuntuk menentukan urutan perhitungan yang jelas



43) potensi bilangan bulat overflow



return 100 / a <= 0 ? a : a + 1 + Random();




Lihat juga
Do not use std::rand() for generating pseudorandom numbers





Dan...
ExitThread function

ExitProcess function

TerminateThread function

TerminateProcess function





Dan itu belum semuanya!



Bayangkan Anda memiliki beberapa konten penting dalam memori (misalnya, sandi pengguna). Tentu saja, Anda tidak ingin menyimpannya dalam memori lebih lama dari yang sebenarnya diperlukan, sehingga meningkatkan kemungkinan seseorang dapat membacanya dari sana .



Pendekatan naif untuk memecahkan masalah ini akan terlihat seperti ini:



bool login(char* const userNameBuf, const size_t userNameBufSize,
           char* const pwdBuf, const size_t pwdBufSize) throw()
{
    if (nullptr == userNameBuf || '\0' == *userNameBuf || nullptr == pwdBuf)
        return false;
    
    // Here some actual implementation, which does not checks params
    //  nor does it care of the 'userNameBuf' or 'pwdBuf' lifetime,
    //   while both of them obviously contains private information 
    const bool result = doLoginInternall(userNameBuf, pwdBuf);
    
    // We want to minimize the time this private information is stored within the memory
    memset(userNameBuf, 0, userNameBufSize);
    memset(pwdBuf, 0, pwdBufSize);
}


Dan itu pasti tidak akan bekerja seperti yang kita inginkan. Apa yang kemudian harus diselesaikan? :(



salah "solusi" # 1: jika memset tidak bekerja, mari kita melakukannya secara manual!



void clearMemory(char* const memBuf, const size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
}


Mengapa ini juga tidak cocok untuk kita? Faktanya adalah bahwa tidak ada batasan dalam kode ini yang tidak memungkinkan kompilator modern untuk mengoptimalkannya (omong-omong, fungsi memset , jika masih digunakan, kemungkinan besar akan menjadi bawaan ).



Lihat juga
The as-if rule

Are there situations where this rule does not apply?

Copy elision

Atomics and optimization



Salah "solusi" # 2: mencoba untuk "meningkatkan" "solusi" sebelumnya dengan bermain-main dengan kata kunci yang mudah menguap



void clearMemory(volatile char* const volatile memBuf, const volatile size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (volatile size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
    
    *(volatile char*)memBuf = *(volatile char*)memBuf;
    // There is also possibility for someone to remove this "useless" code in the future
}


Akankah ini berhasil? Mungkin. Misalnya, pendekatan ini digunakan di RtlSecureZeroMemory (yang dapat Anda lihat sendiri dengan melihat implementasi sebenarnya dari fungsi ini di sumber Windows SDK ).



Namun, teknik ini tidak akan bekerja seperti yang diharapkan dengan semua kompiler .



Lihat juga
volatile member functions



Salah "solusi" # 3: penggunaan yang tidak OS API fungsi (misalnya RtlZeroMemory ) atau STL (misalnya std :: fill, std :: for_each)



RtlZeroMemory(memBuf, memBufSize);


Lebih banyak contoh upaya untuk menyelesaikan masalah ini di sini .



Dan bagaimana itu benar?



  • gunakan fungsi API OS yang benar , misalnya, RtlSecureZeroMemory untuk Windows
  • gunakan fungsi C11 memset_s :


Selain itu, kita dapat mencegah compiler untuk mengoptimalkan kode dengan mencetak (ke file, konsol, atau aliran lain) nilai variabel, tetapi ini jelas tidak terlalu berguna.



Lihat juga
Safe clearing of private Data



Menyimpulkan



Ini, tentu saja, bukan daftar lengkap dari semua kemungkinan masalah, nuansa dan seluk-beluk yang mungkin Anda temui saat menulis aplikasi di C / C ++ .



Ada juga hal-hal hebat seperti:





Dan banyak lagi.







Ada yang ingin ditambahkan? Bagikan pengalaman menarik Anda di kolom komentar!



PS Ingin tahu lebih banyak?
Software security errors

Common weakness enumeration

Common types of software vulnerabilities



Vulnerability database

Vulnerability notes database

National vulnerability database



Coding standards

Application security verification standard

Guidelines for the use of the C++ language in critical systems



Secure programming HOWTO

32 OpenMP Traps For C++ Developers

A Collection of Examples of 64-bit Errors in Real Programs




All Articles