顯示具有 coursera 標籤的文章。 顯示所有文章
顯示具有 coursera 標籤的文章。 顯示所有文章

2018年6月16日 星期六

實作麻雀雖小五臟俱全的程式語言

故事是這樣子的,很早以前曾經看過 understanding computation 這本書,這本書第二章的內容,是利用操作語義(operational semantic)的方式,自訂一款極簡程式語言,非常簡單但已經有 if 判斷式,while 迴圈等功能。
最近剛修完 coursera 上面的 programming language,其中有一個作業也是用 racket 的操作語義定義一款程式語言, 這個程式語言更複雜,在資料結構上支援 pair -> list,同時還支援函式,這是之前 Understanding Computation 沒有實做的部分。
花了幾天的時間,把過去的 code 擴展了一些,在那上面實作了函式,在這裡就簡單介紹一下相關的內容,還有一些個人的心得,心得內容可能有對有錯,如果有錯就請路過野生的大大們指教一下:
要看到原本的內容,可以參考之前發在這個 blog 的筆記,還有 Github 上面的原始碼。
https://yodalee.blogspot.com/2016/03/rust-recursive-structure.html
https://github.com/yodalee/computationbook-rust/tree/master/src/the_meaning_of_programs/simple
本次修改的程式碼則可見:
https://github.com/yodalee/simplelang

所謂操作語義,就是明白的定義程式的各指令實際執行起來是如何,實際執行的機器當然是一台虛擬機,模擬執行的過程。原本書裡面有兩種操作語義的方式,一種是小步 reduce,每次將程式的內容變小一些些;一種則是直接 evaluate,有什麼結果直接計算出來。
要實作函式的部分,我把 reduce 的實作刪掉了,因為我實在不知道要怎麼用 reduce 實作函式,reduce 每次只會拿一個指令,然後把它縮小一些些,但在處理 function 上,只要進到 function 內部就要代換成另一個環境給它,步步執行的 reduce 做不到這點。
因為我的程式是來自於 Understanding Computation,所以裡面有些 code 像 while, sequence 是來自書中,其實我們有了 function ,就可以用 recursive 的方式取代 while,sequence 也可以適度修改,例如把 assign 變成如 let var = val in expression 的形式,在環境裡將一個值賦值給變數之後,以這個環境執行第二個 expression,就不需要 sequence 這樣的語法了;這部分大家自己知道就好。

看一個最簡單的例子:加法。
首先我們定義我們程式的單位:Node,要實作加法需要實作兩種 Node,代表數字和代表加法運算的 Node,接著我們就可以用 Node::number(100) 代表數字,用 Node::add(Node::nubmer(3), Node::number(4)) 代表加法;另外要實做一個 evaluate 函式,讀入一個 Node 並做出對應的操作,例如加法是分別把兩個參數都 evaluate 過,用 value 取值,用我們熟悉的加法加起來;其他運算如乘法、判斷用的大於、小於,都是類似的實作。

再深入一點要實作變數,例如 x = 10,就會需要環境 (environment.rs),它是 evaluate 需要的參數,實作是一個簡單的 HashMap,將變數名稱映射到相對應的 Node 上面,注意是 Node 不是實際地址,在我們的實作下輸入地址都是虛擬化為 Node;如此一來就能定義 Variable 跟 Assign 兩種 Node,分別會從環境中拿出值和將某段程式賦予給某個變數 。

再來就是複雜一些的 Pair, List,先定義 pair 以及兩個輔助用的 fst, snd Node,一個 pair 其實就是把兩個程式組合在一起,之後可以用 fst 跟 snd 取出第一個和第二個的值。
在這裡我們偷懶一下,重複使用 donothing 這個 Node 來表示 list 的結尾,一個 list 即是表示為許多 pair 的嵌套,最後一個 pair 的第二個 Node 必須是 donothing。
使用 iter 搭配 fold 就能輕易將 rust 的 integer vec 轉成我們這個程式語言中的 integer list,從最後一個元素開始,一個一個用 pair 與上一次的結果組合起來。
pub fn vec_to_list(v: Vec) -> Box {
  v.iter().rev()
   .fold(Node::donothing(), |cdr, car| Node::pair(Node::number(*car), cdr))
}
結果:
pair (1, pair (2, pair (3, pair (4, pair (5, do-nothing)))))

這裡有個小小的心得是,程式和資料其實是不可分開的,當我們在組合程式的過程,定義一些特定的組合方式就能將程式轉為資料,反之亦然;雖然這是我偷懶的關係,但 DoNothing 可以是程式,表示不做任何事情,也可以是資料,用來表示 pair 的結尾;程式和資料其實是一體兩面,端視我們執行的時候怎麼處理它。

我們來看一下函數,我們創了三個相關的 Node:
  • Fun:參數是一個 string 作為函數名稱,一個 string 作為變數名稱,還有一個 Node 是函式內部的程式碼。
  • Closure: 我一直很好奇 closure 到底有沒有標準中文翻譯,看到有些翻譯稱為閉包,Closure 是執行時遇到 Func 產生的東西,包含函式內的程式碼,以及 evaluate Fun 時的環境。
  • Call:以給定的參數傳給一個 Closure 並執行。
把 Fun 丟進 evaluate 會把當下的環境打包起來,生成一個 Closure;如果是 Closure 的話就不會特別做什麼。
Call 是最複雜的,它有下列幾個步驟:
  1. 把 closure 跟參數 evaluate 過,如果拿到的不是closure 就會發生錯誤。
  2. 將 closure 儲存的環境跟函式取出來。
  3. 在這個環境中加上兩個新的變數:一個是函數名稱指向自己這個 closure,這樣在函式內 Call 自己的名字就能做到遞迴呼叫的效果; Fun 的參數名稱指向傳進來的變數值。
  4. 用新的環境去 evaluate Fun 所帶的程式。
其實如果寫過 closure ,了解相關的概念之後,再去看下面這個 functionalC 的 repository 就滿好懂的了,如果要實作 Functional Programming 裡面的概念,函式都要自帶一個執行的環境,才能把函式像 first class member 一樣丟來丟去,由於 C 的函式並沒有強制要求這點,所以這一個 repository 裡面才會自己定義 closure 這個 struct ,基本上和我的定義一樣,都包含一個真正要執行的函式,還有執行的環境。
struct closure {
  void *(*fn)(list *);
  list *env;
};
再深入一點,假設程式一開始先設定 1000 個變數,然後再定義一個函式,這個函式要包含的環境是否需要這 1000 個變數?如果完整複製所有環境,太浪費空間了,實際上我們只需要記下函式中真正需要的變數即可,以下面這個函數為例子:
let x_add_y = Node::fun(
  "add1", "y",
  Node::add(Node::variable("x"), Node::variable("y")));
它接受一個參數 y 並將它和變數 x 相加 ,這時只要記錄 x ,其他變數都不需要保存,連變數 y 也不需要,因為 y 一定會在呼叫函式的時候被參數 y 給蓋掉(Shadow)。
變數 x 我們稱之為 free variable ,沒有被程式中的 assign 賦值或是以參數的方式掩蓋掉,它在執行時可視我們的環境設定自由變動。

我在 evaluate.rs 另外寫了一個 cal_free_vars 的函式來計算一段程式內的自由變數,它會生成兩個 HashSet 記錄出現過的變數跟自由變數,然後丟給 helper 函式處理。
大部分的處理方式都差不多,就是一路遞迴往下呼叫,像加法就是分別對兩個 Node 計算內部的自由變數;比較不一樣的是 Variable, Assign, Fun;在 Assign 跟 Fun 的地方,分別要把變數名稱、函數名稱跟參數名稱加到變數名單中,如果 Variable 取用的變數還沒在變數名單內,這個變數就是自由變數。
最後修改 evaluate Fun 的地方:
let free_vars = get_free_vars(self);
let mut new_env = Environment::new();
for var in free_vars {
  new_env.add(&var, env.get(&var));
}
用 get_free_vars 得到函式內的自由變數,生成一個新的環境,從現在的環境取得自由變數的值,用新的環境產生 closure;如果環境中沒有自由變數的值,環境的實作會直接崩潰,強制在執行函式的時候,環境中必須要有它的記錄。
以上大概就是一個有函式的程式語言會需要的實作,基本上這個語言能做到一些相當複雜的事,例如遞迴跟 currying 等,在 evaluate.rs 裡有一些相關的測試,例如階乘的遞迴實作:
fn test_simple_big_function_recursive() {
    let factor = Node::fun("factor", "x", Node::if_cond_else(
            Node::gt(Node::variable("x"), Node::number(1)),
            Node::multiply(Node::variable("x"),
              Node::call(Node::variable("factor"), 
              Node::subtract(Node::variable("x"), Node::number(1)))),
            Node::number(1)));
    let statement = Node::sequence(
        Node::assign("entry", factor),
        Node::assign("result", Node::call(Node::variable("entry"), Node::number(10)))
    );
    let mut env = Environment::new();
    println!("{}", statement.evaluate(&mut env));
    assert_eq!(3628800, env.get("result").value());
}
寫完這款程式語言,我開始覺得其實我是在寫個虛擬機,或者說有點像在實作一台電腦。
我們先把所有的資料:數值、真假值虛擬化為 Node 這個單位,一切的操作都是在 Node 上進行;就像真實的 CPU,也是將數字轉化為記憶體內部的 0, 1,在加法這個指令被執行的時候,會拿出記憶體的內容,通過一連串的邏輯閘變成輸出,再存回記憶體。 記憶體就好比我們的 Node,邏輯閘實作則是 evaluate,CPU 執行 x86 指令這款「語言」對應我們自訂的程式語言,只是CPU是真實的機器,我的程式是一台虛擬機 。
程式其實根基於數學,加法是一個概念,我們只是透過 CPU/電腦的實作,亦或是這篇文的操作語義,用虛擬機模擬出加法這個概念,所謂「程式語言」應是數學之神座下超凡的存在。 我們用上人類智慧的極致,打造 CPU,打造電腦,試著去追趕的這神聖的目標,追之求之,卻稱這款模倣的玩意兒為「程式語言」,何等自傲?想想不禁啞然失笑。

2018年3月4日 星期日

Coursera Introduction to Logic

前些日子開始修了 coursera 上 stanford 大學開的 Introduction to Logic,修完而且有學到東西,其實個人習慣上滿常亂加一些 coursera 上的課程,有些聽一聽覺得無聊就沒聽完,沒聽完的就不會在這裡推薦就是。

下面是基本資訊:
課程名稱:Introduction to Logic
開課學校:Stanford University
授課教授:Michael Genesereth
開課時間:10 周
教學方式:靜態講義
通過方式:每週完成指定的作業,下面會詳述作業內容。

課程內容涵蓋基礎邏輯的概念,符號介紹,邏輯證明跟歸納證明,沒有到太複雜的內容,如果在台灣的大學有修邏輯的話,應該也是差不多的內容,例如通識很熱門的<邏輯丙>,應該內容也差不多。
不像其他課程是教授親自上陣講課,這門課上課的方式是看靜態文字講義(似乎有簡體中文不過我是看英文的),講義看過之後會自動記錄看過,相對來說看靜態文字比看教授講解還要無聊一些,而且有點吃英文能力。
課程內容很大一塊是放在 fitch system,並且證明也都是用 fitch system 來證明,每周作業會出約莫 5 到 10 題的題目,有些是選擇題,選到正確的選項即可;有些則是證明題,要證出所要的結果;有幾週會加一些邏輯遊戲跟參考影片,例如邏輯踩地雷,或者看教授演示一個邏輯表達系統。
證明題使用的是一個 javascript 寫的證明系統,可以用滑鼠選擇敘述,還有要使用的運算來完成證明,一開始有一點難上手,不過熟練之後就會覺得設計真的非常厲害,For all 跟 Exist 的符號代換都會自動完成,點一點就發證明完成了。唯一卡比較大的是在第八週的歸納法,久違地動用紙筆在紙上釐清議義範例證明的思路,想通之後在系統上重現就清楚多了。

學完之後真心覺得有學到東西,我從來沒想過原來連 (p => q) => (~q => ~p), (p => q) => (~p | q) , (p | ~p) 這樣基礎的邏輯也可以證明,但是千真萬確,證過一次才知道這真的可以用邏輯推出來,所以以前說的那些:前題錯就可以推出任何東西,「如果月亮是起司做的,那麼月亮可以吃」都是真的。
其他要注意的事情:寫作業的系統不知道為什麼,用 chrome 開的話會有些問題,用 firefox 來操作就沒問題。
另外,我誠心建議大家睡前不要讀邏輯,讀邏輯當然要沐浴更衣正襟危坐有一次念完之後睡覺,結果在夢裡想歸納證明………,醒來累得半死。

2017年6月17日 星期六

Nand2Tetris 教學投影片與講解影片

故事是這樣子的,5 月的時候我把 Nand2Tetris 這門課給修完了。

剛好,我們同好們辦的 Code& Beer 需要題目,想說都修過了就來講(ㄔㄨㄢˊ)解(ㄐㄧㄠˋ)個 Nand2Tetris 吧,花了點時間整理了投影片,也要感謝在Yahoo 台灣大殺四方驚動萬教每月豪領100K的人生溫拿勝利組強者我同學 qcl 大神,幫我準備場地跟辦活動:

投影片slide share 連結:
https://www.slideshare.net/youtang5/introduction-to-nand2-tetris

錄影使用的是 Facebook 直播,為了把影片載下來花了點功夫,後來找到答案如下:
播放影片之後,按右鍵有一個「顯示影片網址」,將網址的 www 改成 m ,使用 mobile 模式看影片,這時就可以直接右鍵另存影片了:
http://www.green-umbrella.biz/2016/03/how-to-download-facebook-live-stream-videos-into-mp4-files/

當日後面接著 typescript 的活動,這是用 ffmpeg 剪輯過才上傳 Youtube 的:

講了一個半小時,很多地方還是講不清楚,畢竟本來的課程是兩門 coursera,各 6 週,每週的影片時間都是 2 小時以上,這已經是超濃縮版本了,如果大家有興趣還是可以自己修修看,會學到東西的。
不過我想投影片跟錄影完,也算是個總結,我跟 Nand2Tetris 的故事就到這裡了吧,畢竟這也是基礎課程,熟悉了就要往其他更困難的地方前進了。

2017年5月1日 星期一

NAND2Tetris Part2

三月時看到coursera 上,Nand2Teris 第二部分終於開課了,同樣給它選修下去;最近剛把最後的 project 寫完,完成這門課程。
課程網址:
https://www.coursera.org/learn/build-a-computer
另外,這門結束之後,5/8 立刻有開下一個 session,有興趣的大家趕快 Enroll ,一起把電腦給看透透吧。

Nand2Tetris 2 的影片長度明顯比 season 1 長得多,光第一週的影片,要介紹Virtual Machine 概念,又要講解整個 memory layout, pointer access 的概念,第一週影片長達200 分鐘,還不算上 week 0 複習 Nand2Tetris 1 Machine Language 的部分。
另外 part 2 畢竟進到軟體的部分,作業都很吃軟體能力,像是實作 VMTranslator,編譯器,寫高階語言,也不像第一部分,有提供手爆作業給不會寫程式的人,如果沒有基本程式能力選修起來會有些難度。
作業我本來想延續上一個 part 1 都用rust 寫作業,後來…還是偷懶改用Python 寫了(yay,我魯我廢我不會寫 rust QQQQ。

讓我們帶過七週的課程內容:

第一週:

介紹整體 Nand2Tetris stack machine 的架構,整個 Nand2Tetris 都是執行在這種stack machine 上面,之所以用 stack machine 應該是因為相較 register machine,stack machine 還是好做一些,Memory 設定為:
0-4: SP, LCL, ARG, THIS, THAT
5-12: temp
13-15: general register
16-255: static variable
256: stack
2048: heap (以下暫時不用管)
16384: Screen
24576: Keyboard

在 part 1 的最後,我們可以把 assembly 像是 D=A 轉成機械碼 1101...,VM stack machine 程式碼比 assembly 再高階一些,我們可以直接寫stack 操作,例如 push ARG 1,把 Argument 1 推到 stack 的頂端,這樣高階的程式碼讓我們可以更簡單的實作複雜的行為。

作業是VMTranslator,也就是把剛才那些 push ARG 1 轉成一連串的 D=A, D=M 的assembly,寫作業時發現這份文件非常好用:
http://www.dragonwins.com/domains/getteched/csm/CSCI410/references/hack.htm

稍微卡的地方:
pop local 2 這條指令,因為我要先把 local 2 的位址算出來,這樣就需要 D, M, A 三個register (@2, D=A, @LCL, A=M, D=A+D),然後要取SP 的位址也需要三個 register (@SP, A=M, D=M),這樣就沒有地方可以存 pop 出來的東西了
解法是要用到 13-15 的 general purpose register,算出 local 2 位址之後,先塞進 @R13 裡面,取出stack top 放到D 之後就能直接用 @R13, A=M, M=D 將資料塞進 local 2 裡了。

對應課程:我其實不確定,編譯器一般不會是編譯成虛擬機 bytecode ,都是直接變成 bitcode;如果說是stack machine 的觀念的話,那就是虛擬機器了。

第二週:
VM 裡的 branching, function call
branch 很單純,一個影片就講完了;大部分的內容都在講 Function Call,這部分最複雜的也就是 Function call 跟 Return 的calling convention,作業也就實作這兩個功能:
Function Call 大致的內容如下:LCL (local) 對應一般機器上的 base pointer,SP 則是 stack pointer:

呼叫函式的部分:
Push argument
儲存return address, LCL, ARG, THIS, THAT 到 stack 上面
將 ARG 移動到stack 上存參數的位置
將 LCL 到stack 頂端
Jump
寫入Return label

函式本體的部分:
執行Push stack,留給 local 變數空間
函式本體

Return 部分:
將 LCL 儲存到 R13 (general purpose register) 中
將 Return address (LCL - 5) 儲存在 R14 中
複製 stack 頂端的 return value 到 ARG,要注意沒有參數的話,ARG 指向的位置就是 LCL-5 Return address,所以上面要先把 Return address 備份,不然沒有參數的話會被 return value 蓋掉。
將 SP 移到 ARG 的位置 (pop stack)
恢復 LCL, ARG, THIS, THAT
跳到 R14 的 return address

如此就完成了函式的呼叫與返回,有一點要注意的事,講義的部分沒有講很清楚:這週的作業有要寫bootstrap 的部分,他只寫要做到 SP=256; Call Sys.init,其實在Call Sys.init 的部分,也應該要把bootstrap 的 LCL, ARG, THIS, THAT 存到 stack 中,雖然 Sys.init 永遠不會 return,但在實作上還是要求這麼做。
另外我在寫的過程中,如上所述,函式return 時試著去調換 return value 寫入ARG 跟取出return address 的部分,結果就把 return address 蓋掉導致 \explosion/ 了,正好驗證那句:「你以為你會寫程式,假的!叫你用assembly 寫一個Fibonacci 都寫不出來」

對應課程:這周的對應課程我不甚肯定,calling convention 也許是作業系統,也許是編譯器的 code generation 吧,實際可能分散在下面幾堂課裡:
計算機結構、作業系統、計算機組織與組合語言、系統程式。

第三週:
Jack programming language

就是介紹 Jack 的設計,syntax, data struct 之類的怎麼設計的,因為Jack 比起什麼C++/Java 簡單非常非常多,說真的只要學過任何一種 programming language 的,這周應該都可以跳過去不用看。
對應課程:計算機程式。


第四週:

Jack language 的 parser,內容介紹 tokenizer 還有 parse 的基本原理,作業是實做 tokenizer 跟 parser,把 jack program parse 成一堆 token 然後寫到 xml 檔中,要產生 token 的xml ,還有parser 處理過,加上結構節點像是 WhileExpression 的 xml。
tokenizer 我一樣用 python 實作,regular expression 文件裡面有一節就是如何用 re 的 scanner 來寫一個 tokenizer,整個 jack language 的規則大概就是這樣,寫起來輕鬆寫意:
https://docs.python.org/3.2/library/re.html#writing-a-tokenizer
jackkeyword = ['class', 'constructor', 'method', 'function', 'int',
  'boolean', 'char', 'void', 'var', 'static', 'field', 'let', 'do',
  'if', 'else', 'while', 'return', 'true', 'false', 'null', 'this']
tokenSpec = [
  ('comment', r'//[^\n]*|/\*(\*(?!\/)|[^*])*\*/'),
  ('integerConstant', r'\d+'),
  ('symbol', r'[+\-*\/\&\|\<\>\=\~\(\)\{\}\[\]\.\,\;]'),
  ('identifier',  r'[A-Za-z_][A-Za-z_0-9]*'),
  ('stringConstant', r'\"([^"]*)\"'),
  ('newline', r'\n'),
  ('space', r'[ \t]+'),
]

parser 部分我寫了個class ,對各種狀況寫函式來處理,寫得超級醜,文法檢查和輸出的部分都混在一起了,整個 parser 都是用土砲打造而成,正符合下面這句話:
「我一直以為我會寫程式…假的!連個 jack compiler 都寫不出來。」
那個 code 實在是太醜了,決定還是不要公開…,但相較在網路上找到一些人的實作,連文法檢查都沒有,我是覺得我寫得還是很有誠意啦XD
https://github.com/kmanzana/nand2tetris/tree/master/projects/10

對應課程:無疑問的就是編譯器的上半學期


第五週:
Code Generation

上週實作的編譯器只能輸出 xml ,這周要直接把 jack 轉成 VM code,影片就是一步步教你,當遇到運算符號要怎麼處理,呼叫函式要怎麼處理。它的class 沒有 inheritance 所以…嗯,也不用處理 virtual table 的問題,其實滿簡單的。
我覺得收獲最大的,是學到怎麼處理物件 method 的call,其實也就是把指向物件的記憶體位址塞進函式的第一個參數,然後在進到 method 的時候先把這個參數塞進 THIS,這樣就能用 THIS[n] 來取用物件的 data member 了。
這裡也會知道,上面實作時遇到的 THIS, THAT,其實分別對應物件跟陣列,要用物件的 data member 就用 THIS[n] 去取,陣列的內容則用 THAT[n],這樣只需要把記憶體位址存到 THIS, THAT 裡面,就能取用物件和陣列了。

下面是一些實作的筆記:
Jack language 裡面沒有 header/linker 的概念,編譯的時候是以一個 class 為單位在編譯,所以如果裡面出現呼叫其他class 的函式,例如 do obj.foo(),我們只能把它們當成「合法」的程式來產生 code,就算沒有這個 class 或是函式還是可以編譯
但沒有最後一步將所有 vm file link成一個檔案,而是模擬器個別 load 所有的 vm 檔,自然不會像 C/C++ 的 linker ,在最後階段回報 undefined reference to class name/function name 的錯誤。

因為parser 已經把整個程式碼變成 AST了,code generate的部分,只需要把對應的函式寫好,大抵就會動了;沒實作的地方,我發現與其寫 pass 不如直接 raise NotImplementedError,這樣一編譯遇到沒實作的地方,程式就直接 crash 並指出是哪個地方沒有實作;而無論是 parser 還是 code generation, expr->term->factor 這部分的變化是最多元的,這部分處理完,各個 statement 的部分都可以輕鬆對付。

然後…它的模擬程式其實有點兩光,如果選用 program flow 模擬的話會非常慢,依它的建議要把它選成 No Animation,可是這個模式無法編輯記憶體的內容;正確步驟是:load 程式,編譯記憶體,選成 No Animation 執行。
另外,No animation 下,所有對記憶體的操作,都要按下停止模擬才會反映出來,不知道這點的我一直以為我 code generate 哪裡寫錯了lol。

這次作業會需要 call 到一些 library 提供的函式,請參考官方的文件:
http://www.nand2tetris.org/projects/09/Jack%20OS%20API.pdf
例如,要產生字串的話,要用 String.new, String.appendChar 兩個方法,大致如下:
push constant num_of_character
call String.new 1 // 1 argument
for char in string:
  push constant ord(char)
  call String.appendChar 2 //first is string, second is character
最後的 compiler 總共 1000 行左右…覺得實作超糟,都用了python 怎麼還會寫得這麼肥?

對應課程:編譯器的下半學期


第六週:

作業系統,或者稱 Library 或是 runtime 應該比較恰當,總之就是 jack 中,會用到一些 Keyboard, Screen, Math 之類的函式,在之前都是用預先編譯好的模組來執行,現在則要自己用 jack 實作這些功能。這次的影片也高達 230 分,到最後一個 module 連教授都忍不住大叫:Yeah, the last module~~

理論上是用 jack 這樣的高階語言來寫,以為有高階語言就沒事了嗎,錯!
jack 他X 的有夠難寫,少了一個 do, return 什麼的程式就炸給你看(沒有return 它的函式就會一路執行到下一個函式 lol),因為它沒有 operator precedance 所以該加括號的地方也要加,宣告也無法和賦值寫在一起,我大概照他設計的內容,寫完Math 跟 Memory 之後才比較抓到該怎麼寫。
作業寫起來都滿有挑戰性的,像是 Memory.jack 就要用 linked-list 來實作的記憶體管理(用這破破的語言XD)。

作業不止是在考驗寫 jack ,同時也在驗證上一次寫的 compiler ,測試過程中我找到好幾個 compiler 相關的錯誤,像是函式的參數沒處理好,不該傳入 this 的時候傳入 this,讓我在Output 文字的地方 de 了很久的 bug。
這種錯真的超級麻煩,你不一定知道是高階語言寫錯還是compiler 錯…,有時要追著模擬器一步步執行,才能知道是哪裡錯了。這次真的是體會到:好的編譯器帶你上天堂,壞的編譯器帶你住套房。

對應課程:作業系統、系統程式
有一些些的演算法概念(例如乘法要怎麼實作才會快),但份量極少所以不列上好了。

第七週:
這週只是提點一些 Nand2Tetris 可以加強的部分,像是:
在硬體中實作乘法除法來加速,這樣就不需要 library 的函式支援。
在硬體中實作位元運算(左移右移)
如何自己實作模擬器的 built-in chip

另外也公佈可能會有的 Nand2Tetris part 3,如何真的在 FPGA 上實作這台電腦

--

末語:
大推這門課,課程中 Shimon 講課的速度都滿慢的(我都開 1.3-1.4 倍速在聽課),不會太難聽懂。
第二部分相較之下實在太難,覺得還是稍微有碰過程式再來學會比較好,第一部分就沒這問題(也許可以說:電機系的不用修第一部分,資工系的不用修第二部分XDD)修完整部 Nand2Tetris ,對數位電路到組成電腦都有一些概念,第二部分更是讓你看透程式到底怎麼編譯、執行,這點可能即使是資工本科生都不一定全盤了解。
同時,修這門課對我來說也有一點點復健的作用,從簡單的東西入手給自己一點信心:原來,我也是會寫程式,寫得出一些些可以用的東西呢。

僅引用課程講義最後一節的文字:
In CS, God gave us Nand, everything else was done by humans.
神給了我們反及閘,於是我們打造一台電腦。
另外還有影片中教授引用的這段:
We shall not cease from exploration, and the end of all our exploring will be to arrive where we started and know the place for the first time." - T. S. Eliot
作為這門課的總結。

2016年7月21日 星期四

NAND2Tetris Part1

五月的時候看到coursera 上了傳說中鼎鼎大名的課程:Nand2Teris,就給它選修下去了。結果後來遇到七月考N1,課程大停擺(yay,最近才慢慢一週週的把課程聽完。
課程網址:
https://www.coursera.org/learn/build-a-computer
剛發現 7/18-9/4 有再開一次課程,有興趣的大夥進攻囉~~


Week 1: 邏輯閘

介紹Nand是什麼,如何用Nand做出其他邏輯閘,以及課程在Hardware 模擬用的工具和HDL。

作業要用課程提供的Hardware Simulator,從Nand堆出其他的邏輯閘。

這裡說難真的不難,線畫一畫,填個電路就完成了;大部分都是麻煩而已,像是16 路的Or gate,又沒有像verilog 的for loop 或bus 可以用,填接線的數字就飽了(雖然用vim 的block select 可以省一點工夫)

對應課程:交換電路與邏輯設計。


Week 2: ALU

概念講解:用位元表示數字、負數,如何做二進位加法

這個作業要用上星期作業完成的邏輯閘寫一顆ALU出來,ALU的功能規格是固定的。
比較煩人的是,它裡面的使用的HDL 跟平常習慣的Verilog 有點差異,導致在寫作業的時候,它一直報錯
像是它對Bus signal 取值不能在input ,而是在output;同個output 訊號也不能多次使用,這兩個跟verilog 不同。
例如某個mux output 的MSB (15)為sign bit,我們需要輸出sign bit;同時要產生zr 訊號,表示output 是否全為零,在verilog 裡會是類似這樣:
Mux16(out=out, a=result, b=notResult, sel=no);
Or16Way(out=nez, in=out);
Not(out=zr, in=nez);
And(out=ng, a=out[15], b=true);

答案是不行,因為out這個訊號已經當成輸出,不能再接給Or16Way,同時不能在input 的時候對out 訊號這個bus 取它15 的值
正確的寫法會是這樣:
Mux16(out=out, out=out2, out[15]=sign, a=result, b=notResult, sel=no);
Or16Way(out=nez, in=out2);
Not(out=zr, in=nez);
And(out=ng, a=sign, b=true);
知道這個差別,作業同樣不算太難。

對應課程:交換電路與邏輯設計。


Week 3: Sequential logic

介紹Latch 和Flip-flop 兩個sequential 電路,如此一來我們可以用記錄下來的狀態做什麼?最後的perspective 中有介紹如何用Nand 來實作Latch,D Flip-flop 課程中假定為黑盒子。

作業的電路模擬器中內建DFF 和DRegister ,要實作Program Counter, Memory 等電路元件。

RAM 的架構基本上都一樣,寫好最底層的RAM8之後,一路上去的RAM 64, 512, 4K, 16K 都只是複製貼上而已,只是它的Hardward Simulator 怪怪的,有時候會摸擬到 \Explosion/,建議在跑script 的時候可以用慢一點跑.;把veiw/animation選為No Animation也或多或少能防止它爆掉。

Sub bus 用double dot .. 而非Verilog 裡的 comma,同時接線時是接在被assign 那端,例如要把15 pins的wire 接到元件16 pins 的input上。
Element(input[0..14] = wire, input[15] = xxx);

對應課程:交換電路與邏輯設計。


Week 4: 組合語言

上周已經把該有的硬體:ALU,PC,RAM都寫完了,這周離開硬體,開始介紹Machine Language 及組合語言(Hack Assemble),組合語言要如何對應到機器碼,記憶體如何取存用,利用最基本的memory map 對應到screen, keyboard 的操作方式。

這周作業可能比較讓人崩潰,畢竟
Unlike other language like Java, machine language is not designed to make people happy.
不過我還是覺得滿輕鬆的啦XDDD

這周跳脫出來先寫組合語言,下周才會把所有的硬體組起來,寫組合語言等於是知道,機器如何去實習這些功能。

對應課程:計算機結構。


Week 5: 建構一台電腦

介紹電腦整體架構,有這上一週的基礎,我們大致知道電腦會有哪些功能,這周就是要把能夠執行這樣功能的電腦給作出來。
一些基礎元件:Memory, PC, ALU 第三周都已經實現了,這周只是要把它們放起來,寫出自己的CPU跟電腦。

要注意的,電路中包含以下的元件:Memory 的Screen 跟Keyboard取用螢幕跟鍵盤、存程式碼的ROM32K 、以及CPU 裡的DRegister 和ARegister,這些可以在builtInChips裡面找到;要用這些不然硬體模擬器無法抓到這些元件的值。
如果思考一下,就會發現課程都設計好了,控制訊號就是instruction 逐一填入即可,非常輕鬆;想比較多的反而是何是要讓PC jump,但基本上線接一接,Mux 輸入的訊號調整一下就會動了。

對應課程:計算機結構。


Week 6: 組譯器

Part 1 最後一周,介紹組譯器

作業就是寫自幹CPU 的組譯器,什麼語言都可以;為了non-programmer ,課程也設計了手寫作業:人工assembler 給…呃…有毅力的挑戰者。
這部分我使用Rust 來開發,不然我Rust 都快忘光了,被在台灣謀智隻手撐起servo project 半邊天的Rust 台柱呂行大神(又是個好長的稱號XD)屌打,都快無地自容了。
最後當然是寫出來了,可惜只是作業程度,格式錯掉一點點加個空白就會直接噴射,離工業強度大概還差30 dB,超級容易壞掉,叫它草莓組譯器或者玻璃心組譯器(X,這裡也就不拿出來獻醜了。

對應課程:計算機結構,編譯器。


TL;DR

整體來說的話,因為我本身就是電機背景,硬體跟程式都略懂皮毛,寫作業輕鬆愉快,聽課也像在複習忘掉的東西XD
如果沒有相關背景的話也許會困難一點,但修完的那刻,真能領略電腦這複雜又簡明的發明。
能這樣從Nand 往上一路打造出電腦也是很有趣。整體作業非常精巧,沒有多餘的浪費,week 1打造的邏輯閘,在week 2 ALU 、week 3 PC、week 5 CPU 都會用到,不需要特別打造自訂的邏輯閘,一步步照著說明也能自幹一台電腦出來,可以想見規劃這門課程的時候,是真的經過千錘百鍊的設計。

修完這門課,以後看到電腦,儘管它做了那麼多事,接了除了課程沒提到的各種周邊,我們還是可以說:「假的!不過只是相關概念延伸罷了,嚇不倒我滴」

啊不過寫到這就覺得我只不過在小打小鬧,真強者我同學都是跳下去改變世界,哪像我還在這裡聽課程QQQQ

2015年12月20日 星期日

雜湊、訊息鑑別碼與簽章

最近剛翻完Understanding Cryptography這本書,內容真的是淺顯易懂,每章的最後也都會附上相關的參考跟深入研究可讀的參考資料,是本適合當入門的好書,看完之後,想通了雜湊、訊息鑑別碼與簽章三者的差別,在這裡筆記一下,這三個對應的英文分別是 Hash, Message Authentication Code (MAC) 和 Signature,都是訊息傳遞時,用來檢查訊息內容有無「問題」的演算法,三者相似但有些許不同,主要是要對付的問題不一樣。

當我們要傳送一大筆資料時,我們必須要驗證傳送的正確性,沒有錯誤或遭人修改,最基本的就是使用Hash 雜湊,概念就是把傳送端的資料攪一攪,吐出一段代表這份資料的值,接收端再把收到的資料攪一攪,看看出來的值是否一樣。

我們要求Hash 至少要有這些特性:
* 任意長度的輸入,固定長度的輸出
* 要快:如果傳資料要花一秒,算Hash 要十秒,有人要用嗎
* 輸入少量——即便是一個bit 的變動——也應該要讓output 完全不一樣
拿果汁機當比喻,好的果汁機要能攪拌任何輸入的食物並給你一杯新鮮可口的果汁,還要很快攪好,最後,當你攪蘋果汁時加了一些些咖哩,它也要把咖哩分子均勻散佈到每一口蘋果汁裡,打成一杯美味的咖哩蘋果汁(X

另外有Hash三個安全性的要求:
* 從輸出推不出輸入(preimage resistance)
* 給定一個輸入和它的輸出,攻擊者無法產生另一個輸入有同樣的輸出(second preimage resistance)
* 攻擊者無法產生兩個輸入有同樣的輸出(conllision resistance)

知名的Hash如md5, SHA-2,在傳檔案不希望有毀損時,用這些雜湊驗一下是不錯的檢查。

要注意的是,hash 的演算法都是公開的,所以如果有心人Oscar偽造一筆資料跟它的Hash值送給接收端,這筆資料會被誤信為真的資料,Hash 無法對付有心的第三方偽造的攻擊,要對付這種狀況就需要MAC。
MAC和hash 一樣,對一組輸入產生代表的輸出,但MAC除了Hash 的特性外,還會要求一把key,在共同擁有這把的key 的雙方,能驗證訊息完整(integrity),也能確定訊息無法被沒有key的第三方偽造,因此MAC 有時又稱為Keyed Hash Function。
再拿果汁機當例子,共有的key 就像某種獨門配方…像金克拉吧,攪咖哩蘋果汁的時候放點金克拉進去,只有用了金克拉才能吸收土壤兩米下的氮磷鉀攪出金克拉風味咖哩蘋果汁,其他人就無法偽造出這獨特的金克拉風味。

但再進一步,MAC並不提供”不可否認性”,MAC雙方共有這把key,他們都可以產生可被驗證的MAC訊息,這就像有人可以故意打出「有杏仁味的金克拉風味咖哩蘋果汁」然後說是我打的,那我不是冤大頭?這時我們就需要簽章Signature:
簽章跟MAC差異不大,只是我們把hash value 用非對稱演算法的私錀加密過,並公布它的結果,這樣所有有公錀的人都能驗證這份資料是我簽的,卻無法偽造這份簽章;另一方面,我也無法否認這份資料是我簽的,這就是”不可否認性”,這在晶片卡、網路交易上有極大的應用。
就像攪果汁的時候,吐點口水進去,所有人都能驗我的DNA,證實是我攪的果汁;但DNA就只有我有,任何人都無法偽造。

簡而言之,如果你拿訊息過來攪一攪,那是雜湊;攪一攪的同時加上一把通常是對稱性的金錀,這變成MAC;如果產生的過程用上你的私錀,用非對稱加密演算法處理過,就成了簽章。

2015年11月2日 星期一

Coursera cryptography 1

在陰間為了殺時日,總要找點書來看,因為自知不能看一些會變的東西,例如程式Rust;這種東西等我返陽搞不好都翻了兩翻,現在我主力放在兩個方向,一是修coursera 的cryptography 課程,二是閱讀Logan 大大推薦的Enginnering a compiler,前一個cryptography 1 課程剛結束,這裡記錄一下心得。

這門課不算太難,帶過密碼學上幾個重要的概念:Stream cipher, Block cipher, Asymmetirc cipher,人人都怕的數學只在非對稱式加密前帶過一些。
上課的內容還有授課方向,跟這本<understanding cryptography>非常相似,打書名就能找到這本書的全文電子檔;兩者搭配來看,可以補線上課程內容的不足。

程式作業有6題,只有一個是要求用現有的AES lib 來實作CBC 跟CTR 加密,另外五個都是利用已知的漏洞去破解某些密文,就跟教授上課不斷提醒的:”Don’t invent your own ciphers or modes”,作業藉實際攻擊讓大家了解,即使數學理論上安全的加密,在不正確的實作下,仍會導致安全性流失,寫程式會需要一些底子,不過只要會python 或C++應該不算太難。
從程式作業,真的可以體會密碼的設計是一門高深的學問,倒也不是說不要去實作和修改,否則一些知名的lib 像openSSL是誰在維護?而是要先學好基本概念,了解可能的弱點與攻擊方式,多和大家討論分享實作方式,有討論才有進步。

俗話說得好:「三個臭皮匠勝過一個諸葛亮」,密碼學因為攻擊方式千變萬化,絕非一位天才能掌握全部面向,許多科技領域也是如此,在這個年代作為一個工程師,我認為最有效的學習是先學會基本概念,用一些簡單的例子實作驗證這些概念,再去找其他一流的project參與,之所以不要一口氣去跳進一流的project,是因為那裡一定藏了一堆實作細節,會遮掩掉基本概念;千萬不要閉門造車,分享你的想法,才有更大的進步空間。

2015年9月17日 星期四

Cryptography 1:攻擊stream cipher

密碼學中,有一種極簡的密碼,就是stream cipher(流加密XD),對各式的明文,隨機產生一組和它一樣長的key並和明文xor 起來,就是一個夠好的加密,只要該密鑰是隨機產生,如同上一篇所說,密文也會夠隨機。
按:事實上我是聽課才知道這種加密,不像強者我同學曾奕恩大大,完全無師自通,看來我該讓賢了。
在實務上,通常不會用真的random密鑰,因為這會讓密鑰的長度要跟訊息一樣長,不實用,你能想像要先交換一組GB等級的密鑰嗎?我們會用pseudorandom generator,把短密鑰生成為長密鑰,來解決這個問題。

這個加密系統有一大弱點,就是密鑰是一次性的,若一把密鑰重覆使用,而密文遭人攔截,則攻擊者可以利用:
c1 ⊕ c2 = (m1 ⊕ k) ⊕ (m2 ⊕ k) = m1 ⊕ m2
若明文是以ascii 儲存,m1 ⊕ m2 已經有足夠資訊讓人猜出內容。

例如,明文常有的space,ascii 是0x20或32,它跟英文字母xor 起來的結果,大多會落在大小寫轉換的英文字母範圍內,我們用python 來試試:
uppercase = list(range(65,91))
lowercase = list(range(97,123))
msg = uppercase + lowercase
print("".join([chr(c ^ 32) for c in msg]))
print("".join(chr(c) for c in msg))

會得到:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

其實它X的根本就是一個對上一個,這和一般最可能發生的字母xor 字母產生的結果相差頗大,例如a xor z = 27還在不可視字元範圍內,所以如果xor 的結果落在字母內,很可能表示c1, c2中有space。
我們可以先產生一個字典,以常用ascii 和space xor的結果為鍵,以便用來反查該ascii 的值:
xorSpace = {}
for c in msg:
    xorSpace[c^32] = c

然後對各截到的密文,假設是c0, c1 … cn,c0 xor c1 中存在字典中的字元位置,就有可能是c0或c1在這裡有space,將這些位置存下來,跟c0, c2 有space 位置取交集,就能得到c0中space的位置了。
def listspace(c0, c1):
    spacepos = []
    for idx, chars in enumerate(zip(c0, c1)):
        if (chars[0] ^ chars[1]) in xorSpace:
            spacepos.append(idx)
    return spacepos

可以用
c01 = listspace(c0, c1)
c02 = listspace(c0, c2)
list(set(c01).intersection(c02))
輕鬆得到交集結果。

有了space 位置,我們就可以把攻擊對象的字元,一個一個和space 位置xor ,再用先前建的字典轉出可能的字串,例如我們隨便轉一組密文的結果:
Yolal###,##a ##rb###. #eazest xwrcy#c#n th# worl#.

亂碼(我讓它輸出#表示查不到該xor的結果)還不少,但一些字元已足夠我們去猜出明文,例如最後面的the world.;這還只是用只有一組密文的space 位置當標準,如果我們測試所有密文的space位置,結果的開頭如下,每行密文該字元表示和不同密文測試的結果,例如第一行表示第一個字元和另外三個密文測試,兩個沒找到字元,一個轉出Y:
"##Y"
"##o"
"d##dl"
"aaa"
"l"
"#e,,##,#"
"ee#"
"#"
"i,,"
"####s###"
"tt #"
"a"
" "

已經看得出明文大概是’yodalee is a’,如果再用這明文m0 = m1 xor c0 xor c1,可以試著解其它的明文,找出更多粢訊把亂碼的部分消掉,完成攻擊。
例如用下面這段簡短的python code,就可以快速測試已知答案的話,其他密文的內容:
ans0 = "xxx" print("".join([chr(ord(c) ^ c0[i] ^ c1[i]) for i, c in enumerate(ans)])

附帶一提,我加密的訊息是:
"Yodalee is a garbage. Weakest person in the world."
其實不算什麼祕密XD。

2015年9月16日 星期三

Cryptography 1:密碼學裡的隨機

小弟最近消失了好一段時間,都沒在更新文章,其實小弟是掉到陰間去了,在陰間時間變很少,都不寫code(也沒電腦Orz),也不再看相關的東西,例如rust 的文件了,因為我知道一年後出來,rust-lang 搞不好都翻了兩翻,而且看了也不能直接練習,看了等於白看。
最近把時間花在一些比較不會變的東西,像是計算理論、密碼學、數學等等的東西,請我的好友強強林寄列印的書給我,利用放島休的時間聽coursera 上stanford 的cryptography,就算在陰間還是要定期充電,不然出來都變白痴了(雖然說本來就非常弱)。

下面這是聽密碼,關於其中隨機這件事的一些體悟:

密碼學的重點,其實就在「燙」…啊不對,是在「隨機」(random) 兩字,普通的訊息加密變成隨機,讓竊聽者猜不出訊息的內容,才能達成密碼學最重要的目的。
在密碼學裡很常看到xor,就是因為xor 的特性:當你把任何東西X跟隨機的訊息Y xor 起來,出來的東西的機率分佈會跟Y 一樣隨機,因此只要準備一個夠好的Y夠隨機,一個xor 就能把X 給加密好,所以問題就出在Y到底夠不夠隨機上。
當然我們可以用硬體雜訊產生器之類的東西,製造一個真隨機的來源當作密碼,但這在密碼學上不實用,如果是真隨機,表示加、解密雙方無法重現這個密碼,就無法加、解密啦;因此我們需要pseudorandom(偽隨機),用有規則、可重現的方式製造一個很像隨機的東西。

無論是pseudorandom generator/function/permutation,都是設法設計一個夠亂的變法,並讓它們儘可能像random,並讓攻擊者在有限的運算資源限制下,分不出兩者的差異。但總歸來說,兩者還是不同的,以pseudorandom generator 為例,我們會設計很多統計上的測試,像是 0, 1 的數量不能差太多;不能有太長的0序列……讓這些測試無法分出兩者的差異。

千萬不要小看隨機這件事,小小的不隨機,都能讓攻擊者找出一絲絲突破的空間,例如Caesar cipher 隨機性不夠,用頻率攻擊法可以快速破解;enigma 只因為輸入跟輸出不會是同一個字,也能利用這點,配合電子計算機在幾十分鐘內破解;課程中有個例子是DES 有一個問題,可以用線性運算找到一個發生率小於1/2^21 不隨機事件,利用這點,就足以大幅削弱DES 的安全性,也就是說,我們發現這個pseudo-random不是真random,用這個運算預測pseudorandom下一個輸出時,可以比瞎猜好1/2^21。

----

至於random跟pseudorandom是不是一樣呢?

理論上,如果給定無限多組的統計檢定,終究可以驗出真隨機和偽隨機的差異,但同樣的在"有限的運算資源"限制下,能進行的終究只有有限組統計檢定(這個教授倒是有提到,如果P=NP的話,我們就可以……),也就是因為"有限的運算資源"限制,保護了現行的加密系統不被輕易破解。

以真實例子來看,我們可以斷言,pseudorandom不會出現全0這樣罕見的序列,但這樣的序列在random 的狀況是確確實實會發生的,所以pseudorandom 和random 真的有差,但這差異的發生率之小,除非我們能窮盡所有檢查,才能檢出這種差異。
我們在做的事情,就像往霧裡一指,說:來,這是random的產出,然後我們放出另一團pseudorandom 霧,自問:你覺得你這團pseudorandom霧和這random霧像不像?有限的檢查下,我們可能只能驗一下外觀,兩個當然一樣;但給我無限的檢查,會發現random霧裡有幾顆是硫酸、幾顆是氫氧化鈉,他們和你的霧當然不一樣,但把兩團霧混在一起,其實這根本就沒差。