Show HN: Máy tính lập trình được tạo từ cổng NAND
(github.com/ArhanChaudhary)NAND: máy tính 16-bit hoàn chỉnh, tương đương Turing, được hiện thực trên web
- NAND là một máy tính 16-bit tương đương Turing được mô phỏng trên web chỉ với các cổng NAND và xung nhịp
- NAND có CPU, mã máy, assembly, assembler, ngôn ngữ VM, bộ dịch VM, ngôn ngữ lập trình, compiler, IDE và UI riêng
- NAND dựa trên nền tảng Jack-VM-Hack được mô tả trong khóa học Nand to Tetris và cuốn sách liên quan
Ví dụ chương trình
Average
- Một chương trình đơn giản nhận các con số đầu vào và tính giá trị trung bình
- Minh họa luồng điều khiển, phép toán số học, I/O và cấp phát bộ nhớ động
Pong
- Trò chơi Pong dùng để minh họa mô hình hướng đối tượng của ngôn ngữ
- Dùng phím mũi tên để di chuyển thanh đỡ sang trái và phải, đánh bật quả bóng
- Mỗi lần đỡ được bóng thì thanh đỡ sẽ ngắn lại, và trò chơi kết thúc khi bóng rơi xuống dưới màn hình
2048
- Trò chơi 2048 dùng để minh họa đệ quy và logic ứng dụng phức tạp
- Dùng phím mũi tên để di chuyển các con số trên lưới 4x4
- Các số giống nhau sẽ gộp lại khi chạm nhau
- Đạt tới ô 2048 là chiến thắng, nhưng vẫn có thể tiếp tục gom cao hơn
- Trò chơi kết thúc khi bàn cờ đầy và không còn nước đi nào
Overflow
- Một chương trình cố ý gây stack overflow bằng đệ quy vô hạn để thoát khỏi máy ảo
- Tận dụng việc không có kiểm tra runtime để ngăn stack overflow
- Khi chạy, chương trình liên tục in ra giá trị con trỏ stack
- Khi stack chạm tới cuối vùng nhớ dự định và tràn sang bộ nhớ heap, câu lệnh in sẽ bắt đầu lỗi loạn một cách bùng nổ
SecretPassword
- Một chương trình gọi được hàm không thể truy cập bằng cách tận dụng việc runtime không ngăn stack smashing
- Cần hiểu bố cục stack frame của NAND
- Cho phép người dùng ghi đè một địa chỉ bộ nhớ bất kỳ bằng một giá trị tùy ý
- Nếu ghi đè địa chỉ trả về của một hàm thành địa chỉ của hàm khác, có thể thực thi mã tùy ý
- Nếu nhập đúng vị trí bộ nhớ cụ thể và giá trị cần ghi đè, lấy được bằng cách tự kiểm tra địa chỉ stack và assembler, bạn có thể thấy ý tưởng này hoạt động
GeneticAlgorithm
- Một trong rất nhiều thành phần của NAND, và cũng là phần tốn nhiều thời gian phát triển nhất
- Mô phỏng sinh vật sử dụng học máy đơn giản
- “Bộ não” của mỗi điểm là các vector gia tốc, tiến hóa qua chọn lọc tự nhiên để tiến gần mục tiêu
- Ở mỗi thế hệ, các điểm đã “chết” nhưng ở gần mục tiêu hơn có xác suất cao hơn được chọn làm bố mẹ cho thế hệ tiếp theo
- Quá trình sinh sản làm đột biến bộ não, mô phỏng tiến hóa tự nhiên một cách hiệu quả
- Do giới hạn hiệu năng, hệ thống tận dụng nhiều kỹ thuật tối ưu để vượt qua giới hạn phần cứng và biến điều này thành khả thi
Lập trình bằng Jack
- Điều quan trọng nhất khi lập trình bằng Jack là không có độ ưu tiên toán tử. Đây có thể là lý do khiến chương trình không hoạt động đúng.
- Cần dùng dấu ngoặc để chỉ rõ thứ tự ưu tiên như
4 * 2 + 3→(4 * 2) + 3,if (~x & y)→if ((~x) & y)
Giới thiệu về Jack
- NAND tự hào có toàn bộ stack công nghệ của riêng mình
- Jack là một ngôn ngữ hướng đối tượng kiểu yếu. Nói đơn giản thì là C với cú pháp Java
- Hãy học qua một ví dụ
Kiểu dữ liệu tùy chỉnh
- Jack hỗ trợ ba kiểu cơ bản là
int,char,boolean - Có thể mở rộng chúng thành các kiểu dữ liệu trừu tượng khi cần
- Kiến thức về lập trình hướng đối tượng có thể áp dụng ngay
- Trong ví dụ, lớp
Pointđịnh nghĩa một điểm trừu tượng trong không gian - Biến
fieldđược dùng để khai báo thuộc tính theo từng thể hiện của kiểu dữ liệu - Cung cấp các hàm
methodcông khai để người gọi có thể cộng điểm hoặc tính khoảng cách giữa hai điểm - Mọi biến
fieldđều có phạm vi riêng tư mặc định. Muốn truy cập chúng thì phải cung cấp thông qua hàmmethod - Theo thông lệ, lớp dữ liệu nên định nghĩa phương thức
dispose - Tham khảo cú pháp gọi
function,method
Ép kiểu yếu
- Dù đã nói Jack lấy cảm hứng lớn từ Java, nó thực chất chỉ là một lớp vỏ đơn giản
- Java là ngôn ngữ kiểu mạnh và hỗ trợ các tính năng kiểu phức tạp như downcasting, đa hình, kế thừa
- Trong khi đó, Jack thực ra chỉ hỗ trợ một kiểu duy nhất là số nguyên 16-bit có dấu
- Đây là lý do chính khiến Jack là ngôn ngữ kiểu yếu
- Vì vậy compiler của Jack không quá bận tâm nếu bạn trộn các kiểu khác nhau trong phép gán và phép toán
- Jack vẫn cung cấp một mô hình hướng đối tượng mạnh mẽ và hữu dụng
- Điều này sẽ giúp bạn hiểu khi nào và bằng cách nào cần chuyển kiểu
Quản lý bộ nhớ thủ công
- Jack là ngôn ngữ quản lý bộ nhớ thủ công
- Cần cẩn thận giải phóng đúng cách phần bộ nhớ không còn cần nữa
- Nếu không, Jack OS sẽ xem như có rò rỉ bộ nhớ
- Thực hành tốt nhất là viết một phương thức
disposecho mỗi lớp để đóng gói đúng việc giải phóng tài nguyên - Gọi phương thức này khi đối tượng không còn cần thiết nữa sẽ giúp tránh cạn bộ nhớ heap
- Nếu từng dùng các ngôn ngữ quản lý bộ nhớ thủ công khác như C thì điều này sẽ quen thuộc
- Điểm khác là Jack OS lưu mảng và chuỗi trên heap thay vì trên stack
- Xem
String.disposeđể biết cách viết phương thứcdispose
Hành vi không xác định
Độ ưu tiên toán tử
- Quá quan trọng nên được đưa lên trước
Toán tử nhỏ hơn và lớn hơn
- Biểu thức so sánh
a > b,a < btrong Jack có vẻ đơn giản nhưng không phải lúc nào cũng đúng về mặt toán học - Máy ảo biến nó thành
a - b > 0. Nhưnga - bcó thể bị overflow 20000 > -20000sẽ được đánh giá thế nào?20000 - (-20000) > 0, tức-25336 > 0, nên thànhfalse- Nhưng
20000 > -10000thì thành30000 > 0, tứctrue - Nếu chênh lệch trị tuyệt đối giữa
avàblớn hơn 32767 thìa > bvàa < bsẽ sai. Nếu không thì vẫn ổn - Đây không phải bug mà là sự không tương thích với Nand to Tetris. Để giữ tương thích, hành vi này sẽ không được sửa
-32768
- -32768 là con số duy nhất có tính chất đặc biệt là -(-32768) = -32768. Nó là một singleton không có cặp dương tương ứng
- Điều này có thể gây ra các lỗi logic tưởng như vô lý
- Vì toán tử
-xđược xử lý nội bộ thành~(x-1) - Nếu gán
-32768choxthì sẽ thỏax-1 = ~x.~(~x)sẽ trở lại thànhx - Chuyện gì đã xảy ra? Vì NAND là máy 16-bit nên trừ 1 từ -32768 sẽ cho ra kết quả là toàn bộ bit bị đảo
- Điều quan trọng là phải xử lý các lỗi logic trong phép toán số âm
- Việc kiểm tra trường hợp -32768 và xử lý thích hợp là trách nhiệm của lập trình viên
Gọi hàm với thiếu đối số
- Một hành vi không xác định quá hiển nhiên, không cần giải thích
Ép kiểu không phù hợp
- Có thể ép một biến sang bất kỳ kiểu nào bằng
Array - Gọi một phương thức thể hiện không tồn tại sẽ dẫn tới hành vi không xác định
- Compiler không đủ thông minh để phát hiện điều này
Stack overflow
- Xem chương trình Overflow
Sửa stack frame hoặc thanh ghi nội bộ
- Việc sửa stack frame hoặc các thanh ghi nội bộ ở các địa chỉ 256~2047, 1~15 có thể gây ra hành vi không xác định
- Thông thường điều này không thể xảy ra trừ khi lạm dụng
Memory.pokehoặc chỉ số mảng âm - Xem chương trình SecretPassword
Thông số phần cứng
-
Có lý do khiến điện toán 16-bit bị đào thải từ sau thập niên 1970
-
So với 32-bit hay 64-bit, năng lực xử lý và dung lượng bộ nhớ của nó bị hạn chế, không đáp ứng được yêu cầu của phần mềm hiện đại
-
NAND cũng không ngoại lệ
-
So với MacBook 16GiB, NAND chỉ có 4KiB RAM. Tức chỉ bằng 0.00002%
-
Dù vậy, nó vẫn đủ để đưa chúng ta lên Mặt Trăng
-
Jack OS dành ra 14,336 địa chỉ bộ nhớ trong tổng 4KiB cho heap. Một con số cực kỳ nhỏ
-
Vì vậy, ứng dụng Jack phải giải phóng bộ nhớ thật hiệu quả
-
Nếu kế hoạch của bạn quá tham vọng, bạn có thể cạn heap và buộc phải viết lại hoàn toàn kiểu dữ liệu và logic
-
Trong 4KiB đó, 8,192 địa chỉ bộ nhớ được dành cho màn hình
-
Mỗi bit trong mỗi địa chỉ ánh xạ tuyến tính tới pixel trên màn hình 512x256. Dùng đánh số bit LSb 0
-
Địa chỉ bộ nhớ 24,576 được dành cho bàn phím
-
Nó phản ánh mã ASCII của phím đang được nhấn
-
Tuy nhiên, không nên truy cập trực tiếp để xử lý đầu vào người dùng. Hãy dùng lớp Keyboard và các hàm liên quan do Jack OS cung cấp
-
Bàn phím của NAND nhận diện ASCII và các phím đặc biệt
-
240 địa chỉ bộ nhớ được dành cho biến tĩnh của lớp, và 1,792 địa chỉ cho stack toàn cục
-
Trừ khi dùng đệ quy rất sâu, giới hạn này thường không thành vấn đề
1 bình luận
Ý kiến Hacker News