MSIL cơ bản – Part 2/6: Biến cục bộ

Biến là một chức năng không thể thiếu trong bất kì ngôn ngữ lập trình nào. Trong CIL, bạn có thể khai báo và sử dụng biến bằng cách thông qua chỉ số hoặc tên. Tuy nhiên tất cả đều phải thông qua stack nên có thể làm bạn lúng túng với kiểu lập trình này. Trong bài này, tôi sẽ trình bày các vấn đề liên quan đến biến cục bộ và đồng thời qua đó bạn có thể hiểu được cơ chế và cách sử dụng stack một cách hợp lý.

Khai báo và sử dụng biến cục bộ

Để khai báo biến cục bộ trong CIL, bạn sử dụng chỉ thị .locals. Ví dụ để khai báo 3 biến kiểu in32, ta dùng lệnh sau:

.locals init (int32, int32)

Để khai báo biến với tên gọi, bạn thêm tên biến vào sau kiểu dữ liệu:

.locals init (int32 number1, int32 number2)

Muốn gán một giá trị cho biến, trước tiên bạn cần nạp giá trị đó và stack. Trong bài trước bạn đã biết lệnh ldstr dùng để nạp một chuỗi vào stack. Trong bài này vì ta làm việc với các số int32, nên ta phải dùng lệnh ldc.i4 (i4 có nghĩa là integer 4 byte, kiểu int32 có độ lớn 32 bit = 4 byte). Tuy nhiên thay vì nạp sẵn giá trị của biến trong chương trình, tôi muốn các giá trị này phải được nhập từ người dùng, bằng cách sử dụng các phương thức rất quen thuộc mà bạn từng sử dụng trong các ngôn ngữ bậc cao của .Net.

Sau khi nạp giá trị vào stack, bạn phải dùng lệnh stloc (store to local), để lấy giá trị ra khỏi stack và lưu vào biến. Cuối cùng lệnh ldloc (load from local) để nạp một giá trị của biến vào stack.

Bạn có thể truy xuất biến cục bộ theo 2 cách: bằng chỉ số (tính từ 0) và bằng tên biến. Ghi sử dụng chỉ số, bạn có thể ngăn cách giữa lệnh và tham số bằng dấu chấm “.” hoặc khoảng trắng.

Bằng chỉ số: <lệnh>.<chỉ số>             (ví dụ: stloc.0)

Bằng tên: <lệnh> <tên>                       (ví dụ: stloc length)

Mã lệnh hoàn chỉnh cho ví dụ tính diện tích hình chữ nhật bằng CIL:

.assembly extern mscorlib {}
.assembly Rectangle{}

.method static void main()
{
	.entrypoint
	.maxstack 2

	.locals init (int32, int32, int32)

	ldstr &quot;Enter length of rectangle: &quot;
	call void [mscorlib]System.Console::Write(string)
	call string [mscorlib]System.Console::ReadLine()
	call int32 [mscorlib]System.Int32::Parse(string)
	stloc.0

	ldstr &quot;Enter width of rectangle: &quot;
	call void [mscorlib]System.Console::Write(string)
	call string [mscorlib]System.Console::ReadLine()
	call int32 [mscorlib]System.Int32::Parse(string)
	stloc.1

	ldloc.0
	ldloc.1
	mul
	stloc.2

	ldstr &quot;Area of rectangle is: &quot;
	call void [mscorlib]System.Console::Write(string)
	ldloc.2
	call void [mscorlib]System.Console::WriteLine(int32)
	ret
}

Và kết quả sau khi biên dịch và chạy chương trình:

RectangleArea Example

Ta cùng đi phân tích các dòng lệnh trên để xem chúng làm việc như thế nào. Đầu tiên là phần lấy chiều dài của hình chữ nhật nhập từ người dùng:

    ldstr &quot;Enter length of rectangle: &quot;
    call void [mscorlib]System.Console::Write(string)
    call string [mscorlib]System.Console::ReadLine()
    call int32 [mscorlib]System.Int32::Parse(string)
    stloc.0

Dòng đầu tiên và dòng thứ hai như bạn cũng biết, nạp chuỗi “Enter length of rectangle:” vào stack sau đó gọi phương thức Write(string) từ mscorlib để in chuỗi đó ra màn hình. Sau hai dòng lệnh này, stack rỗng.

Dòng thứ 3 gọi phương thức ReadLine() để chờ nhận dữ liệu từ người dùng. Kiểu trả về của phương thức này là string tức là dữ liệu nhập từ người dùng sẽ là kiểu string và được đưa vào stack. Stack lúc này chứa một phần tử kiểu string.

Dòng thứ 4 gọi phương thức Int32::Parse(string), lấy dữ liệu từ đỉnh stack và chuyển sang kiểu int32, kết quả trả về lại đưa vào stack. Stack chứa một phần tử kiểu int32.

Dòng cuối cùng stloc.0, lấy dữ liệu ra khỏi stack và lưu vào biến cục bộ đầu tiên có chỉ số là 0.

Kết quả của đoạn mã này là stack lại trở thành rỗng và biến cục bộ đầu tiên lưu giá trị nhập từ người dùng.

Tiếp tục ta lấy dữ liệu về chiều rộng và lưu vào biến tiếp theo có chỉ số là 1 (stloc.1):

    ldstr &quot;Enter width of rectangle: &quot;
    call void [mscorlib]System.Console::Write(string)
    call string [mscorlib]System.Console::ReadLine()
    call int32 [mscorlib]System.Int32::Parse(string)
    stloc.1

Lúc này stack vẫn ở trạng thái rỗng và 2 biến cục bộ đã có giá trị về chiều dài và chiều rộng của hình chữ nhật. Việc tiếp theo là tính diện tích của hình chữ nhật và lưu vào biến thứ ba (stloc.2):

    ldloc.0
    ldloc.1
    mul
    stloc.2

Đoạn mã trên khá đơn giản, đầu tiên ta dùng ldloc để nạp giá trị của 2 biến cục bộ ở vị trí 0 và 1 vào stack. Sau đó gọi lệnh mul (multiply) để lấy hai phần tử đầu stack và thực hiện phép nhân, kết quả của phép nhân lại được trả về stack. Lúc này stack chỉ có một phần tử, ta lại dùng stloc để lưu kết quả và biến thứ 3.

Lúc này ta đã có đầy đủ dữ liệu cần thiết, chỉ việc in ra màn hình:

    ldstr &quot;Area of rectangle is: &quot;
    call void [mscorlib]System.Console::Write(string)
    ldloc.2
    call void [mscorlib]System.Console::WriteLine(int32)

Sau khi in ra chuỗi “Area of rectangle is: “, ta nạp biến ở vị trí 2 vào stack rồi gọi phương thức WriteLine(int32). Và kết quả như bạn đã thấy ở hình trên.

Bạn thấy rằng trong chương trình này, chỉ có một thời điểm stack lưu nhiều phần tử nhất là lúc nạp 2 biến cục bộ vào để thực hiện phép nhân, tức là giá trị MaxStack ta chỉ cần đặt là 2. Và ta nên tính toán để xác định được MaxStack hợp lý tránh tiêu tốn tài nguyên máy.

Bạn cũng nhận thấy rằng giá trị MaxStack này không phụ thuộc vào số lượng biến ta khai báo và sử dụng, mặc dù ta dùng 3 biến nhưng chỉ cần đặt MaxStack là 2. Tùy thuộc vào cách bạn thực hiện các lệnh và phương thức mà giá trị MaxStack này có thể được tối ưu hay không.

Dùng tên biến thay cho chỉ số

Phiên bản này hoạt động như phiên bản trên chỉ khác là trong mã lệnh, thay vì truy xuất các biến qua chỉ số, ta sẽ dùng tên biến để chương trình rõ ràng hơn.

.assembly extern mscorlib {}
.assembly Rectangle{}

.method static void main()
{
	.entrypoint
	.maxstack 2

	.locals init (int32 length, int32 width, int32 area)

	ldstr &quot;Enter length of rectangle: &quot;
	call void [mscorlib]System.Console::Write(string)
	call string [mscorlib]System.Console::ReadLine()
	call int32 [mscorlib]System.Int32::Parse(string)
	stloc length

	ldstr &quot;Enter width of rectangle: &quot;
	call void [mscorlib]System.Console::Write(string)
	call string [mscorlib]System.Console::ReadLine()
	call int32 [mscorlib]System.Int32::Parse(string)
	stloc width

	ldloc length
	ldloc width
	mul
	stloc area

	ldstr &quot;Area of rectangle is: &quot;
	call void [mscorlib]System.Console::Write(string)
	ldloc area
	call void [mscorlib]System.Console::WriteLine(int32)
	ret
}

Phiên bản không dùng biến

Bạn thấy là mặc dù ta khai báo 3 biến nhưng thực sự ta có thể lưu tất cả dữ liệu trong stack. Như vậy thay vì dùng 3 biến thì ta không cần dùng biến nào cả, giá trị MaxStack vẫn là 2 vì lệnh mul cần 2 phần tử trong stack để thực hiện. Bạn có thể nghĩ rằng MaxStack như vậy là không đủ, tuy nhiên hãy xem chúng ta sẽ thực hiện điều này như thế nào:

.assembly extern mscorlib {}
.assembly Rectangle{}

.method static void main()
{
    .entrypoint
    .maxstack 2

    ldstr &quot;Enter length of rectangle: &quot;
    call void [mscorlib]System.Console::Write(string)
    call string [mscorlib]System.Console::ReadLine()
    call int32 [mscorlib]System.Int32::Parse(string)

    ldstr &quot;Enter width of rectangle: &quot;
    call void [mscorlib]System.Console::Write(string)
    call string [mscorlib]System.Console::ReadLine()
    call int32 [mscorlib]System.Int32::Parse(string)

    mul

    ldstr &quot;Area of rectangle is: &quot;
    call void [mscorlib]System.Console::Write(string)
    call void [mscorlib]System.Console::WriteLine(int32)
    ret
 }

Phiên bản này bạn có thể đọc và hiểu được vì nó tương tự như phiên bản cũ. Ta có thể mô tả vắn tắt cách hoạt động: Đầu tiên đọc giá trị chiều dài và chiều rộng từ người dùng, sau đó dùng lệnh mul để nhân hai giá trị lại, lúc này stack chỉ có một phần tử là diện tích của hình chữ nhật. Tiếp tục, ta nạp chuỗi “Area of rectangle is: “ vào stack và in ra, và cuối cùng dùng WriteLine(int32) in ra phần tử cuối cùng trong stack chính là kết quả cần tìm.

Từ ví dụ này, bạn có thể nhận thấy sự khác biệt của việc lập trình CIL với các ngôn ngữ bậc cao như C#, C++,… Bạn không cần tạo biến để lưu trữ mà có thể tận dụng stack, tuy nhiên cách này đôi khi sẽ chương trình của bạn trở nên rối và khó quản lý, chỉ nên áp dụng trong những chương trình đơn giản.

Bài viết liên quan:

MSIL – Part 1: Giới thiệu về MSIL
MSIL – Part 2: Biến cục bộ
MSIL – Part 3: Rẽ nhánh và vòng lặp
MSIL – Part 4: Lập trình hướng đối tượng
MSIL – Part 5: Conversion, casting và boxing
MSIL – Part 6: Sử dụng mảng

https://yinyangit.wordpress.com

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s