12/08/2018, 00:42

Cùng viết Assembly Language giản lược bằng asmrb gem

I. Giới thiệu chung về assembly Assembly Language là ngôn ngữ "cổ" nhất trong lịch sự, và ngày nay nó gần như trở nên "vô hình" đối với lập trình viên những thế hệ sau này, nhờ sự ra đời của vô số ngôn ngữ hiện đại dựa trên các máy ảo [VM - virtual machine] và trình biên dịch/thông dịch ...

I. Giới thiệu chung về assembly

Assembly Language

là ngôn ngữ "cổ" nhất trong lịch sự, và ngày nay nó gần như trở nên "vô hình" đối với lập trình viên những thế hệ sau này, nhờ sự ra đời của vô số ngôn ngữ hiện đại dựa trên các máy ảo [VM - virtual machine] và trình biên dịch/thông dịch [compiler/interpreter] thông minh.

Virtual Machine

Sự trừu tượng hoá các thành phần bên dưới [low-level abstraction] khiến chúng ta không còn phải giao tiếp trực tiếp với assembly. Thay vào đó, các máy ảo/trình biên dịch sẽ đảm nhận nhiệm vụ biên dịch các ngôn ngữ bậc cao [high-level language] xuống assembly phù hợp với các kiến trúc CPU mà ta lựa chọn như x86, 64, arm ..

Low-Level

Assembly rất gần với phần cứng bên dưới, nó thực hiện phần lớn các tác vụ thông qua các lệnh [instruction], thanh ghi CPU [Register: EAX, EBX...] và bộ nhớ động RAM [Random Access Memory]. Do đặc thù như vậy, nên nó thường được dùng để viết những chức năng cấp dưới, và thiên về phần cứng nhiều hơn là ứng dụng thực sự bên trên.

Stack-based Evaluation

Một cách đơn giản thì assembly có cách thực thi chương trình dựa trên bộ nhớ kiểu ngăn xếp [Stack] giống với các ngôn ngữ hậu thứ tự [PostFix language]:

Thay vì viết: 1 + 2

Ta viết : 1 2 +

Theo kiểu viết của assembly, nó sẽ thực hiện theo trình tự:

push value 1 on stack

push value 2 on stack

call function :add

=> result is 3

Đó là cách ngôn ngữ này thực hiện phần lớn các tác vụ.

Ngoài ra, để rẽ nhánh [branching] và tạo vòng lặp [routine], asmrb sẽ nhảy [jump] đến các nhãn [label] được đánh dấu trong thân chương trình. Tính năng này tương tự với goto ở Basic hay C#.

II. Asmrb - code assembly giản lược trên Ruby

Mình xin giới thiệu gem asmrb của Ruby, một tiện ích giúp bạn làm quen với assembly.

** Cài Đặt **

> gem install asmrb

** Source code **

** Bắt Đầu **

khai báo sử dụng gem asmrb:

require "asmrb"

Tạo một block khi khởi tạo object của Asmrb:

f = Asmrb.new do
....
end

Ok, giờ là code của asmrb. Hãy khai báo 1 hàm mới với fun và tên hàm ở dạng symbol:

f = Asmrb.new do
  fun :factorial
  ret
end

một hàm trong asmrb hay assembly, thường kết thúc bằng ret. Ngoài việc kết thúc chương trình, nó còn trả về giá trị trên cùng [còn gọi là top-most] của stack.

một hàm thường có các tham số [parameter], ta khai báo bằng lệnh arg :acc, :n.

f = Asmrb.new do
  fun :factorial
  arg :acc, :n
  ret
end

Để sử dụng tham số hay giá trị, chúng cần được đẩy lên stack bằng psh:

f = Asmrb.new do
  fun :demo
  psh 1
  psh 2
end
f.invoke
=> [1, 2] > demo

Và đẩy đối tượng top-most ra khỏi stack bằng pop:

  pop
  dbg
end
f.invoke
=> [1, 2] > demo
=> [1] > demo

Quay trở lại hàm :factorial, ta hãy thử xem hàm này trên Ruby:

def factorial acc, n
  if n < 1
    acc
  else
    factorial acc * n, n - 1
  end
end

Như vậy, cần kiểm tra điều kiện n < 1:

true: trả về acc false: gọi factorial với 2 biểu thức acc * n và n - 1

Biểu thức if n < 1

Theo hậu thứ tự: n 1 < dùng psh ta có thể viết:

psh 1
psh :n

Thêm jlt để so sánh n < 1 và nhảy đến block :result nếu thoả mãn điều kiện:

  psh 1
  psh :n
  jlt :result

Code của hàm factorial hiện tại sẽ trở thành:

f = Asmrb.new do
  fun :factorial
  arg :acc, :n
  psh 1
  psh :n
  jlt :result

  ret
end

* Khi điều kiện chưa thoả mãn, asmrb sẽ tiếp tục thực thi các lệnh kế tiếp như vế else của nó vậy.

Để tạo 1 block code, hay thực ra là tạo 1 label để asmrb nhảy đến, ta dùng lệnh blo:

  blo :else

ở đây, ta khởi tạo block :else để tiếp tục phần đệ quy factorial, với 2 biểu thức đã nói: acc * n và n - 1.

Trước hết ta sẽ viết 2 biểu thức này với phép nhân là mul và phép trừ dùng sub:

  psh :n
  psh :acc
  mul
  psh 1
  psh n
  sub

Và giờ gọi lại hàm factorial bằng lệnh rec, nói cách khác, lệnh này luôn tự nhảy về label :factorial được tạo nhờ fun:

  rec

Lúc này, asmrb sẽ thực thi như sau:

  • Giá trị của 2 biểu thức acc * n và n - 1 kia sẽ được lần lượt đẩy lên stack
  • rec sẽ nhảy về block :factorial, và gán 2 giá trị trên cùng của stack cho :acc và :n

Nhìn lại factorial:

f = Asmrb.new do
  fun :factorial
  arg :acc, :n
  psh 1
  psh :n
  jlt :result

  blo :else
  psh :n
  psh :acc
  mul
  psh 1
  psh n
  sub
  rec

  ret
end

Có vẻ khá dài, chúng ta có thể rút ngắn lại bằng cách viết gộp các lệnh psh bằng los:

  psh 1
  psh 2
  psh 3

# became:
  los 1, 2, 3

# evaluation:
=> [1,2,3] > function

factorial sẽ trở thành:

f = Asmrb.new do
  fun :factorial
  arg :acc, :n
  los :n, 1
  jlt :result

  blo :else
  los :acc, :n
  mul
  los :n, 1
  sub
  rec

  ret
end

* los sẽ tự đảo ngược thứ tự phần tử được đẩy lên stack nên ta không cần viết ngược thứ tự như psh.

Cuối cùng, ta viết block :result để trả về giá trị của :acckhi n < 1 thoả mãn:

  blo :result
  psh :acc
  ret
end

Hàm factorial đã hoàn thiện:

f = Asmrb.new do
  fun :factorial
  arg :acc, :n
  los :n, 1
  jlt :result

  blo :else
  los :acc, :n
  mul
  los :n, 1
  sub
  rec

  blo :result
  psh :acc
  ret
end

* Khi muốn xem giá trị hiện tại của stack, ta có thể chèn thêm dbg vào:

f = Asmrb.new do
  fun :demo
  psh 1
  dbg
end
f.invoke
=> [1] > demo

Để chạy hàm này, ở chế độ thông dịch qua asmrb cùng debug cách bước chạy, thêm phần:

f.is_debug = true
result = f.invoke 1, 3
puts result

kết quả ở console:

----------------------
[1, 3] > factorial
0: arg acc n
1: los n 1
2: jlt result
3: los acc n
4: mul
5: los n 1
6: sub
7: rec

::[factorial]
0: arg acc n
1: los n 1
2: jlt result
3: los acc n
4: mul
5: los n 1
6: sub
7: rec

::[factorial]
0: arg acc n
1: los n 1
2: jlt result
3: los acc n
4: mul
5: los n 1
6: sub
7: rec

::[factorial]
0: arg acc n
1: los n 1
2: jlt result

::[result]
8: psh acc
9: ret

----------------------
6

Ở console-log trên, ta có thể thấy:

  • chương trình nhận tham số [1, 3] > factorial,
  • Sau đó chạy đến khi gặp lệnh rec, và nhảy về đầu chương trình ::[factorial] ,
  • Cứ thế lặp đến khi nhảy đến block ::[result]
  • Nó trả lại kết quả về stack và thoát chương trình.
6

Ta nhận đc kết quả phép tính sau khi invoke là 6.

III. Tổng quan

Trên đây chỉ là một ví dụ về cách viết một chương trình assembly đã được "giản lược" đi rất nhiều thành phần [ như Register, Memory Pointer, Memory Address ...] để nó trở nên đơn giản và gần gũi hơn với ngôn ngữ bậc cao.

Hy vọng các bạn sẽ tìm thấy nhiều điều thú vị với gem này. Do đang trong giai đoạn demo bản mẫu nên asmrb chưa thật sự hoàn thiện. Mọi ý kiến / đóng góp xin đừng ngần ngại gửi về github của trên, mình xin cảm ơn (bow)

0