Thứ Sáu, 18 tháng 3, 2011

// // Leave a Comment

Mã không an toàn trong C#

1. Managed & Unmanaged code

- Managed code là mã được thực thi dưới sự giám sát cửa CLR(Common Language Runtime). CLR có nhiệm vụ giống như là 1 lao công, như:
  • Quản lý bộ nhớ cho các đối tượng
  • Xác minh kiểu đang được thực thi
  • Thu gom rác (các biến mà chúng ta bầy bừa ra trong quá trình viết mã)
Người dùng (ở đây là lập trình viên) hoàn toàn bị cô lập với các nhiệm vụ trên. và anh ta không thể thao tác trực tiếp  lên bộ nhớ, vì tất cả công việc đó được thực hiện bởi CLR
- Trong khi đó, mã không được quản lý Unmanaged code được thực thi bên ngoài sự kiểm soát của CLR.
Ví dụ đẽ thấy là các thư viên Win32 DLLs của chúng ta như kernel32.dll, user32.dll, và các thành phần COM được cài đặt trên hệ thống. Giống như trong một chương trình C++ chương trình cấp phát bộ nhớ cho một con trỏ là một ví dụ của mã không được quản lý bởi vì chính lập trình viên phải có trách nhiệm:
  • Gọi hàm cấp phát bộ nhớ
  • Đảm bảo đúng khuôn mẫu
  • Đảm bảo bộ nhớ được giải phóng mỗi khi xong nhiệm vụ

2. Unsafe code

- Mã không an toàn (unsafe) là kiểu lai ghép giữa Managed code và Unmanaged code
- Nó thực hiện dưới sự giám sát của CLR giống như Managed code, nhưng cho phép bạn thao tác địa chỉ trên bộ nhớ một cách trực tiếp thông qua việc sử dụng con trỏ giống hệt như mã không được quản lý Unmanaged code. Cho bạn có sự lựa chọn tốt nhất giữa hai cách này

3. Let's get coding

- Để viết unsafe code ta sử dụng 2 từ khóa đặc biệt: unsafe fixed. Và sử dụng 3 toán tử con trỏ
  1. *
  2. &
  3. ->
Để sử dụng các toán tử con trỏ này thì ta cần khai báo và sử dụng chúng trong khối lệnh unsafe
unsafe {

// Sử dụng fixed và các toán tử con trỏ
}

Ngoài ra bạn có thể khai báo unsafe code cho biến toàn cục, phương thức, cho class, hay struct.
Ví dụ:
public static unsafe void binhPhuong(int* number)
{
       *number = (*number) * 2;

}

4. Xác định địa chỉ của con trỏ

Cú pháp khai báo:
int* pWidth, pHeight;
double* pResult;
Ký tự * kết hợp với kiểu hơn là với biến -> như ở trên là ta khai báo 2 biến con trỏ kiểu int

Ép kiểu con trỏ sang kiểu int:
Vì con trỏ là 1 số int lưu địa chỉ nên ta có thể ép kiểu sang kiểu int một cách tường minh. Ví dụ:
int x = 10;
int *pX, pY;
pX = &x;
pY = pX;
*pY = 20;
uint y = (uint)pX;
int *pR = (int*)y;


Ép kiểu giữa các con trỏ
byte aByte = 8;
byte* pByte = &aByte;
double* pDouble = (double*)pByte;

Con trỏ void
void *pointerToVoid;
pointerToVoid = (void*)pointerToInt;   // pointerToInt declared as int*
* Mục đích sử dụng khi gọi các hàm API yêu cầu tham số void*.


Toán tử sizeof
Cho phép lấy số byte của kiểu đó, lưu ý là chỉ dùng cho kiểu Struct không chứa dữ liệu tham chiếu. Ví dụ:
int x = sizeof(double);


Con trỏ kiểu Struct
- Cũng giống như con trỏ trong các kiểu dữ liệu sẵn có. Con trỏ có thể trỏ đến cấu trúc với điều kiện là Struct đó không chứa bất kỳ kiểu tham chiếu nào. Do con trỏ không thể trỏ đến bất kỳ kiểu tham chiếu nào, trình biên dịch sẽ phát lỗi nếu ta sử dụng con trỏ đến bất kỳ Struct nào có chứa kiểu tham chiếu.

- Ta khai báo 1 struct như sau:
struct MyStruct
{
    public long X;
    public float F;
}

- Định nghĩa con trỏ pStruct
MyStruct* pStruct;

- Khởi tạo:
MyStruct struct = new MyStruct();pStruct = &struct;

- 2 cách truy xuất giá trị thành viên của 1 struct bằng con trỏ
(*pStruct).X = 4;
(*pStruct).F = 3.4f
// Hoặc
pStruct->X = 4;
pStruct->F = 3.4f;
Con trỏ trỏ đến các thành viên của lớp
Vì rằng ta không thể tạo ra con trỏ cho 1 lớp. Tuy nhiên ta có thể tạo các con trỏ đến các thành viên của nó


class MyClass
{
    public long X;
    public float F;
} 

// Sử dụng lớp vừa khai báo, với cách truy nhập thông thường để trỏ đến các thành viên X & F của nó thì việc làm này sẽ gây ra lỗi??
        MyClass myObj = new MyClass();                    
        long* pL = &(myObj.X);   // wrong
        float* pF = &(myObj.F);  // wrong
Do X và F nằm trong 1 lớp, mà nó lại được đặt trong vùng nhớ HEAP. Nghĩa là chùng chịu sự quản lý của bộ gom rác, cụ thể bộ gom rác có thể di chuyển MyClass đến 1 vị trí mới trong bộ nhớ để dọn dẹp Heap. Nếu là điều này thì bộ gom rác tất nhiên sẽ cập nhật lại các tham chiếu đến đối tượng.

Giả sử như myObj vẫn sẽ trỏ đến đúng vị trí. Tuy nhiên việc rọn dẹp các đối tượng không sử dụng nữa, giải phóng bộ nhớ và cập nhật lại các đối tượng tham chiếu. Trong khi đó pL & pF vẫn không thay đổi giá trị tham chiếu của nó và kết cuộc là trỏ đến sai vị trí vùng nhớ (myObj đã dọn sang nhà mới mà không báo cho pL & pF biết).

Để giải quyết vấn đề này ta dùng từ khóa fixed để cố định vùng nhớ và thông báo cho bộ gom rác không được phép di chuyển vùng nhớ chứa các thể hiện của lớp này. Cú pháp thực hiện như sau:

MyClass myObj = new MyClass();
// do whatever
fixed (long *pObject = &( myObj.X))
{
   // do something
}

Nếu muốn khai báo nhiều hơn một con trỏ thì ta có thể đặt các khối lện fixed lồng nhau như dưới đây:


MyClass myObj = new MyClass();
fixed (long *pX = &( myObj.X))
{
   // do something with pX
   fixed (float *pF = &( myObj.F))
   {
      // do something else with pF
   }
}

Hoặc có thể kết hợp khởi tạo nhiều biến trong cùng 1 khối lệnh fixed

MyClass myObj = new MyClass();
MyClass myObj = new MyClass();
fixed (long *pX = &( myObj.X), pX2 = &( myObj.X))
{
   // etc.

    }
}

5. Dùng con trỏ để tối ưu hóa thực thi

Sử dụng stackalloc để cấp phát vùng nhớ, cú pháp:
decimal *pDecimal = stackalloc decimal [10];
lệnh này chỉ đơn giản là cấp phát vùng nhớ mà không khởi tạo bất kỳ giá trị nào
đồng thời pDecimal trỏ đến ô nhớ đầu tiên của mảng 10 số Decimal
Các thao tác tính toán với con trỏ trỏ đến mảng thực hiện tương tự như trong C hay C++
Bên cạnh đó C# còn cho phép sử dụng chỉ số phần tử Indexer thay cho cú pháp thông thường. Nếu p là con trỏ và i là kiểu số nguyên thì biểu thức p[i] tương đương với *(p + i). Ví dụ:
double *pDoubles = stackalloc double [20];
pDoubles[0] = 3.0;   // pDoubles[0] is the same as *pDoubles
pDoubles[1] = 8.4;   // pDoubles[1] is the same as *(pDoubles+1)

0 comments: