Có thể đánh giá lập trình an toàn cho mã tùy ý?


9

Gần đây tôi đã suy nghĩ rất nhiều về mã an toàn. Chủ đề an toàn. Bộ nhớ an toàn. Không-sẽ-nổ-trong-mặt-với-một-một-segfault an toàn. Nhưng để rõ ràng trong câu hỏi, hãy sử dụng mô hình an toàn của Rust như định nghĩa của chúng tôi.

Thông thường, đảm bảo an toàn là một vấn đề lớn, bởi vì, như đã được chứng minh bởi nhu cầu của Rust unsafe, có một số ý tưởng lập trình rất hợp lý, như đồng thời, không thể được thực hiện trong Rust mà không sử dụng unsafetừ khóa . Mặc dù đồng thời có thể được thực hiện hoàn toàn an toàn với các khóa, đột biến, kênh và cách ly bộ nhớ hoặc những gì có bạn, nhưng điều này đòi hỏi phải làm việc bên ngoài mô hình an toàn của Rust với unsafe, và sau đó đảm bảo trình biên dịch rằng, "Vâng, tôi biết tôi đang làm gì Điều này có vẻ không an toàn, nhưng tôi đã chứng minh về mặt toán học là nó hoàn toàn an toàn. "

Nhưng thông thường, điều này bắt nguồn từ việc tạo ra các mô hình những thứ này một cách thủ công và chứng minh rằng chúng an toàn với các quy tắc định lý . Từ góc độ khoa học máy tính (có thể) và quan điểm thực tiễn (nó sẽ lấy đi sự sống của vũ trụ), có hợp lý không khi tưởng tượng một chương trình lấy mã tùy ý bằng ngôn ngữ tùy ý và đánh giá liệu nó có hay không " Rỉ an toàn "?

Hãy cẩn thận :

  • Một cách dễ dàng để làm điều này là chỉ ra rằng chương trình có thể không được giải quyết và do đó vấn đề tạm dừng làm chúng tôi thất bại. Giả sử bất kỳ chương trình nào được cung cấp cho người đọc đều được đảm bảo tạm dừng
  • Mặc dù "mã tùy ý trong một ngôn ngữ tùy ý" là mục tiêu, tôi tất nhiên nhận thức được rằng điều này phụ thuộc vào sự quen thuộc của chương trình với ngôn ngữ đã chọn, chúng tôi sẽ sử dụng như một ngôn ngữ nhất định

2
Mã tùy ý? Không. Tôi tưởng tượng bạn thậm chí không thể chứng minh sự an toàn của hầu hết các mã hữu ích vì I / O và ngoại lệ phần cứng.
Telastyn

7
Tại sao bạn không quan tâm đến vấn đề dừng lại? Mỗi một ví dụ bạn đã đề cập, và nhiều ví dụ khác, đã được chứng minh là tương đương với việc giải quyết vấn đề dừng, vấn đề chức năng, Định lý Rice, hoặc bất kỳ định lý bất khả thi nào khác: an toàn con trỏ, an toàn bộ nhớ, luồng - An toàn, ngoại lệ, an toàn, tinh khiết, an toàn I / O, an toàn khóa, bảo đảm tiến độ, v.v ... Vấn đề dừng là một trong những tính chất tĩnh đơn giản nhất có thể bạn có thể muốn biết, mọi thứ khác bạn liệt kê khó hơn nhiều .
Jörg W Mittag

3
Nếu bạn chỉ quan tâm đến dương tính giả và sẵn sàng chấp nhận phủ định sai, tôi có một thuật toán phân loại mọi thứ: "Có an toàn không? Không"
Caleth 8/11/18

Bạn hoàn toàn không cần sử dụng unsafeRust để viết mã đồng thời. Chúng là một số cơ chế khác nhau có sẵn, từ nguyên thủy đồng bộ hóa đến các kênh lấy cảm hứng từ diễn viên.
RubberDuck

Câu trả lời:


8

Điều cuối cùng chúng ta đang nói ở đây là thời gian biên dịch so với thời gian chạy.

Biên dịch lỗi thời gian, nếu bạn nghĩ về nó, cuối cùng sẽ giúp trình biên dịch có thể xác định những vấn đề bạn gặp phải trong chương trình trước khi nó chạy. Đây rõ ràng không phải là trình biên dịch "ngôn ngữ tùy ý", nhưng tôi sẽ sớm quay lại với nó. Trình biên dịch, trong tất cả sự khôn ngoan vô hạn của nó, tuy nhiên không liệt kê mọi vấn đề có thể được xác định bởi trình biên dịch. Điều này phụ thuộc một phần vào trình biên dịch được viết tốt như thế nào, nhưng lý do chính cho điều này là rất nhiều thứ được xác định khi chạy .

Lỗi thời gian chạy, như bạn đã quen thuộc với tôi chắc chắn như tôi, là bất kỳ loại lỗi nào xảy ra trong quá trình thực thi chương trình. Điều này bao gồm chia cho số không, ngoại lệ con trỏ null, vấn đề phần cứng và nhiều yếu tố khác.

Bản chất của lỗi thời gian chạy có nghĩa là bạn không thể lường trước các lỗi đã nói tại thời gian biên dịch. Nếu bạn có thể, họ gần như chắc chắn sẽ được kiểm tra tại thời điểm biên dịch. Nếu bạn có thể đảm bảo một số bằng 0 tại thời điểm biên dịch, thì bạn có thể thực hiện một số kết luận logic nhất định, chẳng hạn như chia bất kỳ số nào cho số đó sẽ dẫn đến lỗi số học gây ra bởi chia cho 0.

Như vậy, theo một cách rất thực tế, kẻ thù của chương trình đảm bảo hoạt động đúng của chương trình đang thực hiện kiểm tra thời gian chạy trái ngược với kiểm tra thời gian biên dịch. Một ví dụ về điều này có thể là thực hiện chuyển động sang loại khác. Nếu điều này được cho phép, bạn, lập trình viên, về cơ bản sẽ ghi đè khả năng của trình biên dịch để biết liệu đó có phải là điều an toàn không. Một số ngôn ngữ lập trình đã quyết định rằng điều này có thể chấp nhận được trong khi những ngôn ngữ khác ít nhất sẽ cảnh báo bạn vào thời gian biên dịch.

Một ví dụ điển hình khác có thể cho phép null là một phần của ngôn ngữ, vì ngoại lệ con trỏ null có thể xảy ra nếu bạn cho phép null. Một số ngôn ngữ đã loại bỏ hoàn toàn vấn đề này bằng cách ngăn chặn các biến không được khai báo rõ ràng để có thể giữ các giá trị null được khai báo mà không được gán ngay một giá trị (lấy ví dụ về Kotlin). Mặc dù bạn không thể loại bỏ lỗi thời gian chạy ngoại lệ con trỏ null, bạn có thể ngăn nó xảy ra bằng cách loại bỏ tính chất động của ngôn ngữ. Trong Kotlin, bạn có thể buộc khả năng giữ các giá trị null tất nhiên, nhưng không cần phải nói rằng đây là một "người mua hãy cẩn thận" ẩn dụ vì bạn phải nói rõ ràng như vậy.

Về mặt khái niệm, bạn có thể có một trình biên dịch có thể kiểm tra lỗi trong mọi ngôn ngữ không? Có, nhưng nó có thể sẽ là một trình biên dịch cồng kềnh và không ổn định, trong đó bạn nhất thiết phải cung cấp ngôn ngữ được biên dịch trước. Nó cũng không thể biết nhiều điều về chương trình của bạn, ngoài các trình biên dịch cho các ngôn ngữ cụ thể biết một số điều nhất định về nó, chẳng hạn như vấn đề tạm dừng như bạn đã đề cập. Hóa ra, rất nhiều thông tin có thể thú vị để tìm hiểu về một chương trình là không thể lượm lặt được. Điều này đã được chứng minh, vì vậy nó không có khả năng thay đổi bất cứ lúc nào sớm.

Trở về điểm chính của bạn. Phương pháp không tự động luồng an toàn. Có một lý do thực tế cho điều này, đó là các phương thức an toàn của luồng cũng chậm hơn ngay cả khi các luồng không được sử dụng. Rust quyết định rằng họ có thể loại bỏ các vấn đề thời gian chạy bằng cách làm cho luồng phương thức an toàn theo mặc định và đó là lựa chọn của họ. Nó đi kèm với một chi phí mặc dù.

Có thể về mặt toán học có thể chứng minh tính đúng đắn của một chương trình, nhưng sẽ là một lời cảnh báo rằng bạn sẽ có các tính năng thời gian chạy bằng không trong ngôn ngữ. Bạn sẽ có thể đọc ngôn ngữ này và biết những gì nó làm mà không có bất ngờ. Ngôn ngữ có thể trông rất toán học trong tự nhiên, và đó có thể không phải là ngẫu nhiên ở đó. Nhắc nhở thứ hai là lỗi thời gian chạy vẫn xảy ra, có thể không liên quan gì đến chính chương trình. Do đó, chương trình có thể được chứng minh là đúng, giả sử một loạt các giả định về máy tính của nó đang được chạy trên là chính xác và không thay đổi, trong đó tất nhiên luôn không xảy ra anyway và thường xuyên.


3

Loại hệ thống được tự động kiểm chứng bằng chứng về một số khía cạnh của tính chính xác. Ví dụ, hệ thống loại của Rust có thể chứng minh rằng một tham chiếu không tồn tại lâu hơn đối tượng được tham chiếu hoặc đối tượng được tham chiếu không bị sửa đổi bởi một luồng khác.

Nhưng hệ thống loại khá hạn chế:

  • Họ nhanh chóng chạy vào các vấn đề quyết định. Đặc biệt, bản thân hệ thống loại phải có thể quyết định được, tuy nhiên nhiều hệ thống loại thực tế vô tình Turing Complete (bao gồm C ++ vì các mẫu và Rust vì các đặc điểm). Ngoài ra, một số thuộc tính nhất định của chương trình mà họ đang xác minh có thể là không thể giải quyết được trong trường hợp chung, nổi tiếng nhất là liệu một số chương trình tạm dừng (hoặc phân kỳ).

  • Ngoài ra, các hệ thống loại nên chạy nhanh, lý tưởng trong thời gian tuyến tính. Không phải tất cả các bằng chứng có thể được đưa vào hệ thống loại. Ví dụ, toàn bộ phân tích chương trình thường được tránh và bằng chứng được phân chia theo các mô-đun hoặc chức năng đơn lẻ.

Do những hạn chế này, các hệ thống loại có xu hướng chỉ xác minh các thuộc tính khá yếu, dễ chứng minh, ví dụ: hàm được gọi với các giá trị đúng loại. Tuy nhiên, ngay cả điều đó hạn chế đáng kể tính biểu cảm, do đó, thông thường có các cách giải quyết (như interface{}trong Go, dynamictrong C #, Objecttrong Java, void*trong C) hoặc thậm chí sử dụng các ngôn ngữ tránh hoàn toàn việc gõ tĩnh.

Các thuộc tính mạnh hơn mà chúng tôi xác minh, ngôn ngữ thường sẽ ít biểu cảm hơn. Nếu bạn đã viết Rust, bạn sẽ biết những khoảnh khắc này chiến đấu với trình biên dịch, trong đó trình biên dịch từ chối mã có vẻ đúng, bởi vì nó không thể chứng minh tính đúng. Trong một số trường hợp, không thể diễn tả một chương trình nào đó trong Rust ngay cả khi chúng tôi tin rằng chúng tôi có thể chứng minh tính đúng đắn của nó. Cơ unsafechế trong Rust hoặc C # cho phép bạn thoát khỏi giới hạn của hệ thống loại. Trong một số trường hợp, trì hoãn kiểm tra thời gian chạy có thể là một tùy chọn khác - nhưng điều này có nghĩa là chúng tôi không thể từ chối một số chương trình không hợp lệ. Đây là một vấn đề định nghĩa. Một chương trình Rust gây hoảng loạn là an toàn khi có liên quan đến hệ thống loại, nhưng không nhất thiết phải theo quan điểm của một lập trình viên hoặc người dùng.

Ngôn ngữ được thiết kế cùng với hệ thống loại của họ. Rất hiếm khi một hệ thống loại mới được áp đặt cho một ngôn ngữ hiện có (nhưng xem ví dụ MyPy, Flow hoặc TypeScript). Ngôn ngữ sẽ cố gắng làm cho nó dễ dàng viết mã phù hợp với hệ thống loại, ví dụ bằng cách đưa ra các chú thích loại hoặc bằng cách giới thiệu các cấu trúc dòng điều khiển dễ dàng để chứng minh. Các ngôn ngữ khác nhau có thể kết thúc với các giải pháp khác nhau. Ví dụ: Java có khái niệm về finalcác biến được gán chính xác một lần, tương tự như các mutbiến không của Rust :

final int x;
if (...) { ... }
else     { ... }
doSomethingWith(x);

Java có các quy tắc hệ thống loại để xác định xem tất cả các đường dẫn gán biến hay chấm dứt hàm trước khi biến có thể được truy cập. Ngược lại, Rust đơn giản hóa bằng chứng này bằng cách không có các biến được khai báo nhưng không được ký, nhưng cho phép bạn trả về các giá trị từ các câu lệnh điều khiển:

let x = if ... { ... } else { ... };
do_something_with(x)

Điều này có vẻ như là một điểm thực sự nhỏ khi tìm ra sự phân công, nhưng phạm vi rõ ràng là cực kỳ quan trọng đối với các bằng chứng liên quan đến suốt đời.

Nếu chúng ta áp dụng một hệ thống kiểu Rust cho Java, chúng ta sẽ gặp nhiều vấn đề lớn hơn thế: các đối tượng Java không được chú thích với thời gian sống, vì vậy chúng ta sẽ phải coi chúng là &'static SomeClasshoặc Arc<dyn SomeClass>. Điều đó sẽ làm suy yếu bất kỳ bằng chứng kết quả. Java cũng không có khái niệm bất biến cấp độ loại vì vậy chúng ta không thể phân biệt giữa &&mutloại. Chúng ta sẽ phải coi bất kỳ đối tượng nào là một Cell hoặc Mutex, mặc dù điều này có thể đảm bảo các bảo đảm mạnh hơn Java thực sự cung cấp (thay đổi một trường Java không phải là chủ đề an toàn trừ khi được đồng bộ hóa và biến động). Cuối cùng, Rust không có khái niệm về kế thừa triển khai theo kiểu Java.

TL; DR: hệ thống loại là các định lý định lý. Nhưng chúng bị giới hạn bởi các vấn đề có thể quyết định và mối quan tâm về hiệu suất. Bạn không thể đơn giản lấy một hệ thống loại và áp dụng nó cho một ngôn ngữ khác, vì cú pháp ngôn ngữ của mục tiêu có thể không cung cấp thông tin cần thiết và vì ngữ nghĩa có thể không tương thích.


3

Làm thế nào an toàn là an toàn?

Có, gần như có thể viết một trình xác minh như vậy: chương trình của bạn chỉ cần trả về UNSAFE không đổi. Bạn sẽ đúng 99% thời gian

Bởi vì ngay cả khi bạn chạy một chương trình Rust an toàn, ai đó vẫn có thể rút phích cắm trong quá trình thực thi: vì vậy chương trình của bạn có thể bị dừng ngay cả khi về mặt lý thuyết là không được phép.

Và ngay cả khi máy chủ của bạn đang chạy trong một chiếc lồng xa trong hầm, một quy trình hàng xóm có thể thực hiện khai thác búa và thực hiện một cú lật trong một trong những chương trình Rust được cho là an toàn của bạn.

Điều tôi đang cố gắng nói là phần mềm của bạn sẽ chạy trong một môi trường không xác định và nhiều yếu tố bên ngoài có thể ảnh hưởng đến việc thực thi.

Đùa sang một bên, xác minh tự động

Đã có các máy phân tích mã tĩnh có thể phát hiện ra các cấu trúc lập trình rủi ro (các biến chưa được khởi tạo, tràn bộ đệm, v.v ...). Chúng hoạt động bằng cách tạo một biểu đồ chương trình của bạn và phân tích sự lan truyền của các ràng buộc (loại, phạm vi giá trị, trình tự).

Nhân tiện, loại phân tích này cũng được thực hiện bởi một số trình biên dịch nhằm mục đích tối ưu hóa.

Chắc chắn có thể tiến thêm một bước, đồng thời phân tích đồng thời và đưa ra những suy luận về sự lan truyền ràng buộc qua một số chủ đề, đồng bộ hóa và điều kiện đua xe. Tuy nhiên, rất nhanh bạn sẽ gặp phải vấn đề bùng nổ tổ hợp giữa các đường thực thi và nhiều ẩn số (I / O, lập lịch hệ điều hành, đầu vào của người dùng, hành vi của các chương trình bên ngoài, gián đoạn, v.v.) sẽ giải quyết các ràng buộc đã biết tối thiểu và làm cho rất khó để đưa ra bất kỳ kết luận tự động hữu ích nào về mã tùy ý.


1

Turing đã giải quyết vấn đề này vào năm 1936 với bài viết của ông về vấn đề tạm dừng. Một trong những kết quả là, không thể viết một thuật toán mà 100% thời gian có thể phân tích mã và xác định chính xác liệu nó có dừng lại hay không, không thể viết một thuật toán có thể chính xác 100% thời gian xác định xem mã có bất kỳ thuộc tính cụ thể nào hay không, bao gồm cả "an toàn" tuy nhiên bạn muốn xác định nó.

Tuy nhiên, kết quả của Turing không loại trừ khả năng chương trình có thể 100% thời gian (1) hoàn toàn xác định mã là an toàn, (2) hoàn toàn xác định rằng mã đó không an toàn hoặc (3) đưa tay lên hình người và nói "Heck, tôi không biết." Trình biên dịch của Rust, nói chung, là trong thể loại này.


Vì vậy, miễn là bạn có một tùy chọn không chắc chắn, có phải không?
TheEnvironmentalist

1
Điều đáng nói là luôn luôn có thể viết một chương trình có khả năng gây nhầm lẫn cho một chương trình phân tích chương trình. Sự hoàn hảo là không thể. Thực tiễn có thể có thể.
NovaDenizen

1

Nếu một chương trình là toàn bộ (tên kỹ thuật của một chương trình được bảo đảm tạm dừng), thì về mặt lý thuyết có thể chứng minh bất kỳ tài sản tùy ý nào trên chương trình được cung cấp đủ tài nguyên. Bạn chỉ có thể khám phá mọi trạng thái tiềm năng mà chương trình có thể nhập và kiểm tra xem có bất kỳ trạng thái nào vi phạm tài sản của bạn không. Các TLA + ngôn ngữ mô hình kiểm tra sử dụng một biến thể của phương pháp này, sử dụng lý thuyết tập hợp để kiểm tra tính của bạn chống lại bộ của các quốc gia chương trình tiềm năng, chứ không phải tính toán tất cả các nước.

Về mặt kỹ thuật, bất kỳ chương trình nào được thực hiện trên bất kỳ phần cứng vật lý thực tế nào đều là tổng số hoặc là một vòng lặp có thể chứng minh được do thực tế bạn chỉ có một lượng lưu trữ hạn chế, do đó chỉ có một số trạng thái hữu hạn mà máy tính có thể ở. máy tính thực sự là một máy trạng thái hữu hạn, không phải là Turing hoàn chỉnh, nhưng không gian trạng thái quá lớn nên dễ giả vờ rằng chúng đang hoàn tất).

Vấn đề với cách tiếp cận này là nó có độ phức tạp theo cấp số nhân đối với lượng lưu trữ và kích thước của chương trình, khiến nó không thực tế đối với bất kỳ thứ gì ngoài cốt lõi của thuật toán và không thể áp dụng cho toàn bộ cơ sở mã quan trọng.

Vì vậy, phần lớn các nghiên cứu tập trung vào bằng chứng. Thư từ Curry Curry Howard nói rằng một bằng chứng về tính đúng đắn và một hệ thống loại là một và giống nhau, vì vậy hầu hết các nghiên cứu thực tế đều mang tên hệ thống loại. Đặc biệt có liên quan đến cuộc thảo luận này là CoqIdriss, ngoài Rust mà bạn đã đề cập. Coq tiếp cận vấn đề kỹ thuật cơ bản từ hướng khác. Lấy bằng chứng về tính chính xác của mã tùy ý trong ngôn ngữ Coq, nó có thể tạo mã thực thi chương trình đã được chứng minh. Idriss trong khi đó sử dụng một hệ thống loại phụ thuộc để chứng minh mã tùy ý trong một ngôn ngữ giống như Haskell thuần túy. Những gì cả hai ngôn ngữ này làm là đẩy các vấn đề khó khăn trong việc tạo ra một bằng chứng khả thi cho người viết, cho phép trình kiểm tra loại tập trung vào việc kiểm tra bằng chứng. Kiểm tra bằng chứng là một vấn đề đơn giản hơn nhiều, nhưng điều này khiến các ngôn ngữ khó làm việc hơn nhiều.

Cả hai ngôn ngữ này được thiết kế đặc biệt để làm cho bằng chứng dễ dàng hơn, sử dụng độ tinh khiết để kiểm soát trạng thái nào có liên quan đến phần nào của chương trình. Đối với nhiều ngôn ngữ chính, việc chỉ chứng minh rằng một phần trạng thái không liên quan đến bằng chứng về một phần của chương trình có thể là một vấn đề phức tạp do bản chất của tác dụng phụ và giá trị đột biến.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.