Memahami SAL

Bahasa anotasi kode sumber Microsoft (SAL) menyediakan serangkaian anotasi yang dapat Anda gunakan untuk menjelaskan bagaimana fungsi menggunakan parameternya, asumsi yang dibuatnya tentang mereka, dan jaminan yang dibuatnya ketika selesai. Anotasi ditentukan dalam file <sal.h>header . Analisis kode Visual Studio untuk C++ menggunakan anotasi SAL untuk memodifikasi analisis fungsinya. Untuk informasi selengkapnya tentang SAL 2.0 untuk pengembangan driver Windows, lihat Anotasi SAL 2.0 untuk Driver Windows.

Secara asli, C dan C++ hanya menyediakan cara terbatas bagi pengembang untuk secara konsisten mengekspresikan niat dan invariansi. Dengan menggunakan anotasi SAL, Anda dapat menjelaskan fungsi Anda secara lebih rinci sehingga pengembang yang mengonsumsinya dapat lebih memahami cara menggunakannya.

Apa itu SAL dan Mengapa Anda Harus Menggunakannya?

Cukup dinyatakan, SAL adalah cara murah untuk membiarkan kompilator memeriksa kode Anda untuk Anda.

SAL Membuat Kode Lebih Berharga

SAL dapat membantu Anda membuat desain kode Anda lebih dapat dimengerti, baik untuk manusia maupun untuk alat analisis kode. Pertimbangkan contoh ini yang menunjukkan fungsi memcpyruntime C :

void * memcpy(
   void *dest,
   const void *src,
   size_t count
);

Dapatkah Anda mengetahui fungsi ini? Ketika fungsi diimplementasikan atau dipanggil, properti tertentu harus dipertahankan untuk memastikan kebenaran program. Hanya dengan melihat deklarasi seperti yang ada dalam contoh, Anda tidak tahu apa itu. Tanpa anotasi SAL, Anda harus mengandalkan komentar dokumentasi atau kode. Berikut adalah dokumentasi untuk memcpy mengatakan:

"memcpy salinan menghitung byte dari src ke dest; wmemcpy salinan menghitung karakter lebar (dua byte). Jika sumber dan tujuan tumpang tindih, perilaku memcpy tidak ditentukan. Gunakan memmove untuk menangani wilayah yang tumpang tindih.
Penting: Pastikan bahwa buffer tujuan berukuran sama atau lebih besar dari buffer sumber. Untuk informasi selengkapnya, lihat Menghindari Buffer Overruns."

Dokumentasi berisi beberapa bit informasi yang menunjukkan bahwa kode Anda harus mempertahankan properti tertentu untuk memastikan kebenaran program:

  • memcpycount menyalin byte dari buffer sumber ke buffer tujuan.

  • Buffer tujuan harus setidaknya sebesar buffer sumber.

Namun, pengkompilasi tidak dapat membaca dokumentasi atau komentar informal. Tidak tahu bahwa ada hubungan antara kedua buffer dan count, dan itu juga tidak dapat secara efektif menebak tentang hubungan. SAL dapat memberikan lebih banyak kejelasan tentang properti dan implementasi fungsi, seperti yang ditunjukkan di sini:

void * memcpy(
   _Out_writes_bytes_all_(count) void *dest,
   _In_reads_bytes_(count) const void *src,
   size_t count
);

Perhatikan bahwa anotasi ini menyerupai informasi dalam dokumentasi, tetapi lebih ringkas dan mengikuti pola semantik. Saat membaca kode ini, Anda dapat dengan cepat memahami properti fungsi ini dan cara menghindari masalah keamanan buffer yang diserbu. Lebih baik lagi, pola semantik yang disediakan SAL dapat meningkatkan efisiensi dan efektivitas alat analisis kode otomatis dalam penemuan awal potensi bug. Bayangkan seseorang menulis implementasi buggy ini dari wmemcpy:

wchar_t * wmemcpy(
   _Out_writes_all_(count) wchar_t *dest,
   _In_reads_(count) const wchar_t *src,
   size_t count)
{
   size_t i;
   for (i = 0; i <= count; i++) { // BUG: off-by-one error
      dest[i] = src[i];
   }
   return dest;
}

Implementasi ini berisi kesalahan umum off-by-one. Untungnya, pembuat kode menyertakan anotasi ukuran buffer SAL—alat analisis kode dapat menangkap bug dengan menganalisis fungsi ini saja.

Dasar-Dasar SAL

SAL mendefinisikan empat jenis parameter dasar, yang dikategorikan berdasarkan pola penggunaan.

Kategori Anotasi Parameter Deskripsi
Input ke fungsi yang dipanggil _In_ Data diteruskan ke fungsi yang disebut, dan diperlakukan sebagai baca-saja.
Input ke fungsi yang dipanggil, dan output ke pemanggil _Inout_ Data yang dapat digunakan diteruskan ke fungsi dan berpotensi dimodifikasi.
Output ke pemanggil _Out_ Pemanggil hanya menyediakan ruang untuk fungsi yang disebut untuk ditulis. Fungsi yang disebut menulis data ke dalam ruang tersebut.
Output pointer ke pemanggil _Outptr_ Seperti Output ke pemanggil. Nilai yang dikembalikan oleh fungsi yang disebut adalah penunjuk.

Keempat anotasi dasar ini dapat dibuat lebih eksplisit dengan berbagai cara. Secara default, parameter penunjuk yang dianotasi diasumsikan diperlukan—parameter tersebut harus non-NULL agar fungsi berhasil. Variasi anotasi dasar yang paling umum digunakan menunjukkan bahwa parameter penunjuk bersifat opsional—jika NULL, fungsi masih dapat berhasil melakukan pekerjaannya.

Tabel ini memperlihatkan cara membedakan antara parameter yang diperlukan dan opsional:

Parameter diperlukan Parameter bersifat opsional
Input ke fungsi yang dipanggil _In_ _In_opt_
Input ke fungsi yang dipanggil, dan output ke pemanggil _Inout_ _Inout_opt_
Output ke pemanggil _Out_ _Out_opt_
Output pointer ke pemanggil _Outptr_ _Outptr_opt_

Anotasi ini membantu mengidentifikasi kemungkinan nilai yang tidak diinisialisasi dan penggunaan penunjuk null yang tidak valid secara formal dan akurat. Meneruskan NULL ke parameter yang diperlukan dapat menyebabkan crash, atau dapat menyebabkan kode kesalahan "gagal" dikembalikan. Bagaimanapun, fungsi tidak dapat berhasil dalam melakukan pekerjaannya.

Contoh SAL

Bagian ini memperlihatkan contoh kode untuk anotasi SAL dasar.

Menggunakan Alat Analisis Visual Studio Code untuk Menemukan Cacat

Dalam contoh, alat Analisis Visual Studio Code digunakan bersama dengan anotasi SAL untuk menemukan cacat kode. Berikut adalah cara untuk melakukan itu.

Untuk menggunakan alat analisis kode Visual Studio dan SAL

  1. Di Visual Studio, buka proyek C++ yang berisi anotasi SAL.

  2. Pada bilah menu, pilih Bangun, Jalankan Analisis Kode pada Solusi.

    Pertimbangkan contoh _In_ di bagian ini. Jika Anda menjalankan analisis kode di atasnya, peringatan ini ditampilkan:

    C6387 Nilai Parameter Tidak Valid 'pInt' bisa menjadi '0': ini tidak mematuhi spesifikasi untuk fungsi 'InCallee'.

Contoh: Anotasi _In_

Anotasi _In_ menunjukkan bahwa:

  • Parameter harus valid dan tidak akan dimodifikasi.

  • Fungsi ini hanya akan membaca dari buffer elemen tunggal.

  • Pemanggil harus menyediakan buffer dan menginisialisasinya.

  • _In_ menentukan "baca-saja". Kesalahan umum adalah menerapkan _In_ ke parameter yang seharusnya memiliki _Inout_ anotasi sebagai gantinya.

  • _In_ diizinkan tetapi diabaikan oleh penganalisis pada skalar non-pointer.

void InCallee(_In_ int *pInt)
{
   int i = *pInt;
}

void GoodInCaller()
{
   int *pInt = new int;
   *pInt = 5;

   InCallee(pInt);
   delete pInt;
}

void BadInCaller()
{
   int *pInt = NULL;
   InCallee(pInt); // pInt should not be NULL
}

Jika Anda menggunakan Analisis Visual Studio Code pada contoh ini, ini memvalidasi bahwa penelepon meneruskan penunjuk non-Null ke buffer yang diinisialisasi untuk pInt. Dalam hal ini, pInt pointer tidak boleh NULL.

Contoh: Anotasi _In_opt_

_In_opt_ sama dengan _In_, kecuali bahwa parameter input diizinkan untuk menjadi NULL dan, oleh karena itu, fungsi harus memeriksa ini.

void GoodInOptCallee(_In_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
   }
}

void BadInOptCallee(_In_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
}

void InOptCaller()
{
   int *pInt = NULL;
   GoodInOptCallee(pInt);
   BadInOptCallee(pInt);
}

Analisis Visual Studio Code memvalidasi bahwa fungsi memeriksa NULL sebelum mengakses buffer.

Contoh: Anotasi _Out_

_Out_ mendukung skenario umum di mana penunjuk non-NULL yang menunjuk ke buffer elemen diteruskan dan fungsi menginisialisasi elemen. Pemanggil tidak perlu menginisialisasi buffer sebelum panggilan; fungsi yang disebut menjanjikan untuk menginisialisasinya sebelum kembali.

void GoodOutCallee(_Out_ int *pInt)
{
   *pInt = 5;
}

void BadOutCallee(_Out_ int *pInt)
{
   // Did not initialize pInt buffer before returning!
}

void OutCaller()
{
   int *pInt = new int;
   GoodOutCallee(pInt);
   BadOutCallee(pInt);
   delete pInt;
}

Alat Analisis Kode Visual Studio memvalidasi bahwa pemanggil meneruskan penunjuk non-NULL ke buffer untuk pInt dan bahwa buffer diinisialisasi oleh fungsi sebelum kembali.

Contoh: Anotasi _Out_opt_

_Out_opt_ sama dengan _Out_, kecuali bahwa parameter diizinkan menjadi NULL dan, oleh karena itu, fungsi harus memeriksa ini.

void GoodOutOptCallee(_Out_opt_ int *pInt)
{
   if (pInt != NULL) {
      *pInt = 5;
   }
}

void BadOutOptCallee(_Out_opt_ int *pInt)
{
   *pInt = 5; // Dereferencing NULL pointer 'pInt'
}

void OutOptCaller()
{
   int *pInt = NULL;
   GoodOutOptCallee(pInt);
   BadOutOptCallee(pInt);
}

Analisis Visual Studio Code memvalidasi bahwa fungsi ini memeriksa NULL sebelum pInt didereferensikan, dan jika pInt bukan NULL, bahwa buffer diinisialisasi oleh fungsi sebelum dikembalikan.

Contoh: Anotasi _Inout_

_Inout_ digunakan untuk membuat anotasi parameter penunjuk yang dapat diubah oleh fungsi. Penunjuk harus menunjuk ke data yang diinisialisasi yang valid sebelum panggilan, dan bahkan jika berubah, penunjuk harus tetap memiliki nilai yang valid saat dikembalikan. Anotasi menentukan bahwa fungsi dapat dengan bebas membaca dari dan menulis ke buffer satu elemen. Pemanggil harus menyediakan buffer dan menginisialisasinya.

Catatan

Seperti _Out_, _Inout_ harus berlaku untuk nilai yang dapat dimodifikasi.

void InOutCallee(_Inout_ int *pInt)
{
   int i = *pInt;
   *pInt = 6;
}

void InOutCaller()
{
   int *pInt = new int;
   *pInt = 5;
   InOutCallee(pInt);
   delete pInt;
}

void BadInOutCaller()
{
   int *pInt = NULL;
   InOutCallee(pInt); // 'pInt' should not be NULL
}

Analisis Visual Studio Code memvalidasi bahwa penelepon meneruskan penunjuk non-NULL ke buffer yang diinisialisasi untuk pInt, dan bahwa, sebelum kembali, pInt masih non-NULL dan buffer diinisialisasi.

Contoh: Anotasi _Inout_opt_

_Inout_opt_ sama dengan _Inout_, kecuali bahwa parameter input diizinkan untuk menjadi NULL dan, oleh karena itu, fungsi harus memeriksa ini.

void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
      *pInt = 6;
   }
}

void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
   *pInt = 6;
}

void InOutOptCaller()
{
   int *pInt = NULL;
   GoodInOutOptCallee(pInt);
   BadInOutOptCallee(pInt);
}

Analisis Visual Studio Code memvalidasi bahwa fungsi ini memeriksa NULL sebelum mengakses buffer, dan jika pInt bukan NULL, bahwa buffer diinisialisasi oleh fungsi sebelum kembali.

Contoh: Anotasi _Outptr_

_Outptr_ digunakan untuk membuat anotasi parameter yang dimaksudkan untuk mengembalikan pointer. Parameter itu sendiri tidak boleh NULL, dan fungsi yang disebut mengembalikan pointer non-NULL di dalamnya dan pointer tersebut menunjuk ke data yang diinisialisasi.

void GoodOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 5;

   *pInt = pInt2;
}

void BadOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   // Did not initialize pInt buffer before returning!
   *pInt = pInt2;
}

void OutPtrCaller()
{
   int *pInt = NULL;
   GoodOutPtrCallee(&pInt);
   BadOutPtrCallee(&pInt);
}

Analisis Visual Studio Code memvalidasi bahwa penelepon meneruskan penunjuk non-NULL untuk *pInt, dan bahwa buffer diinisialisasi oleh fungsi sebelum kembali.

Contoh: Anotasi _Outptr_opt_

_Outptr_opt_ sama dengan _Outptr_, kecuali bahwa parameter bersifat opsional—pemanggil dapat meneruskan penunjuk NULL untuk parameter .

void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;

   if(pInt != NULL) {
      *pInt = pInt2;
   }
}

void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;
   *pInt = pInt2; // Dereferencing NULL pointer 'pInt'
}

void OutPtrOptCaller()
{
   int **ppInt = NULL;
   GoodOutPtrOptCallee(ppInt);
   BadOutPtrOptCallee(ppInt);
}

Analisis Visual Studio Code memvalidasi bahwa fungsi ini memeriksa NULL sebelumnya *pInt didereferensikan, dan bahwa buffer diinisialisasi oleh fungsi sebelum kembali.

Contoh: Anotasi _Success_ dalam Kombinasi dengan _Out_

Anotasi dapat diterapkan ke sebagian besar objek. Secara khusus, Anda dapat membuat anotasi seluruh fungsi. Salah satu karakteristik fungsi yang paling jelas adalah dapat berhasil atau gagal. Tetapi seperti hubungan antara buffer dan ukurannya, C/C++ tidak dapat mengekspresikan keberhasilan atau kegagalan fungsi. Dengan menggunakan _Success_ anotasi, Anda dapat mengatakan seperti apa keberhasilan untuk fungsi. Parameter untuk _Success_ anotasi hanyalah ekspresi bahwa ketika itu benar menunjukkan bahwa fungsi telah berhasil. Ekspresi bisa menjadi apa pun yang dapat ditangani pengurai anotasi. Efek anotasi setelah fungsi kembali hanya berlaku ketika fungsi berhasil. Contoh ini menunjukkan bagaimana _Success_ berinteraksi dengan _Out_ untuk melakukan hal yang benar. Anda dapat menggunakan kata kunci return untuk mewakili nilai yang dikembalikan.

_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
   if(flag) {
      *pInt = 5;
      return true;
   } else {
      return false;
   }
}

Anotasi _Out_ menyebabkan Analisis Visual Studio Code memvalidasi bahwa pemanggil meneruskan penunjuk non-NULL ke buffer untuk pInt, dan bahwa buffer diinisialisasi oleh fungsi sebelum kembali.

Praktik Terbaik SAL

Menambahkan Anotasi ke Kode yang Ada

SAL adalah teknologi canggih yang dapat membantu Anda meningkatkan keamanan dan keandalan kode Anda. Setelah belajar SAL, Anda dapat menerapkan keterampilan baru ke pekerjaan harian Anda. Dalam kode baru, Anda dapat menggunakan spesifikasi berbasis SAL berdasarkan desain di seluruh; dalam kode yang lebih lama, Anda dapat menambahkan anotasi secara bertahap dan dengan demikian meningkatkan manfaat setiap kali Anda memperbarui.

Header publik Microsoft sudah diannotasikan. Oleh karena itu, kami menyarankan agar dalam proyek Anda, Anda terlebih dahulu membuat anotasi fungsi dan fungsi simpul daun yang memanggil API Win32 untuk mendapatkan manfaat maksimal.

Kapan saya Membuat Anotasi?

Berikut adalah beberapa panduan:

  • Buat anotasi semua parameter penunjuk.

  • Anotasi anotasi rentang nilai sehingga Analisis Kode dapat memastikan keamanan buffer dan pointer.

  • Membuat anotasi aturan penguncian dan mengunci efek samping. Untuk informasi selengkapnya, lihat Anotasi Perilaku Penguncian.

  • Anotasi properti driver dan properti khusus domain lainnya.

Atau Anda dapat membuat anotasi semua parameter untuk memperjelas niat Anda di seluruh dan memudahkan untuk memeriksa apakah anotasi telah dilakukan.

Baca juga