2014年11月21日 星期五

[books] - SICP 2nd edition

全名是: Structure and Interpretation of Computer Programs
簡體中文書名: 计算机程序的构造和解释
20131024 淘寶訂單 20131104 到手, 12 天才收到, 有點久。
費用 42 * 5.1 = 214.2 + 145 = 359.2 委託代買購得。

這本書有電子版本, 當然是英文的, 要不然我就不用花錢買中文的版本。

Peter Norvig, paul graham 給了 5 星評價。
簡體中文版本看來薄薄的, 可別以為是本小書, 整整有 472 頁, 所以紙質真的不怎麼樣 (薄的厲害, 我覺得用印表機得到的品質都比這本書好), 在上頭寫注記得小心點, 一不小心可能就劃破紙張。在閱讀上也有點困擾, 整本書歪斜的很厲害, 不好拿著看。

簡體中文版本出版日期: 2004 年 2 月 1 日, 我這本是 201306 第八刷, 原文書 1984 年出版,是美国麻省理工学院 (MIT) 多年使用的一本教材, 1996 年修订为第二版。

後來 MIT 开始用 python 教授此课,代替了原本的 Scheme: Why MIT switched from Scheme to Python

淘寶有些書是影印的版本, 還好這本不是, 我本來還擔心這本已經出版了這麼久的一段時間, 會買不到正版的, 不過看來這本書還有陸續再出版。

我的進度很緩慢, 不過我很有耐心和毅力, 忍住開發 os, qt for android, uefi 程式的相關誘惑, 專心看這本書, 慢慢地, 慢慢地, 我越來越可以看下去了, 速度也提升了一點點。她有什麼魔力讓我願意這樣做呢? ref 2 提到: 您没法从中学到如何开发一个网站,开发一个记事本,如何绘图,它完全是在锻炼程序员的基本能力,而不是“技术”。一言以蔽之, 就是在教怎麼寫程式。很推薦大一同學和程式初學者讀這本, 這本書並不是進階書而是入門書哦! 但這不表示它很好讀, 請有心理準備。和一般的程式語言書籍不同, 裡頭的數學味道很重, 幾乎都是數學題目, 牛頓法、最大公因數、微分, 有理數/複數的運算, 真是不習慣。更不習慣的是他所選用的語言是 lisp 系的 scheme, prefix syntax 讓人迷惑, 這是吸引我的最主要部份, 據說 2008 年已經改用 python (好像是 2009), 若是 python, 我也許就沒興趣了吧! 看著一堆小括號充滿螢幕, 真是有趣。

4.1.1 ~ 4.1.4 會寫一個 scheme interpreter, 我覺得還是照課本使用 scheme 來讀 sicp, 比較不會有和課本出入的地方。

這本教科書我讀來實在辛苦, 覺得很難, 所以讀得很慢, 不知道是不是因為簡體中文的翻譯, 我覺得又讓她更難讀了。译者裘宗燕老師學問應該很好, 不過翻譯可能不是他的強項 (他翻譯過很多不錯的原文書籍)。

In a production-quality Lisp system,
書中翻成 "在產品質量的 Lisp 系統裡", 說實在, 我得看原文才懂這句話。這英文不算難懂, 但要翻譯成通順的句子那就不容易了。

It calls apply-primitive-procedure to apply primitives;
書上翻譯: 它直接調用 apply-primitive-procedure 去應用基本過程
我把它改為: 它直接呼叫 apply-primitive-procedure 去執行 primitives procedure。

To define a variable, we search the first frame for a binding for the variable
我們需要在第一個框架查找該變量的約束
descent 改: 我們需要在第一個框架查找該變數的定義

還真的不好翻譯, 只看中文會看不懂, 查詢完原文後就好多了。

This computation will run much more slowly than a gcd procedure written in
Scheme, because we will simulate low-level machine instructions, such as
assign, by much more complex operations.

書上翻譯為:
因為我們是在模擬低級的機器指令, 例如 assign, 而使用的卻是比他高級得多的操作。
這句話也是怪怪的。

書中的句子有很多像是這樣的翻譯, 讀來不能算是輕鬆, 我偶爾得對照一下原文詞句。

就算翻譯的不算好, 但中文版本還是幫了我不少忙, 這本並沒有爛到完全看不懂, 大多時候還是可以理解翻譯後的意思, 沒有中文翻譯, 我無法那麼快瀏覽過一遍, 有些翻不好的地方如果不是關鍵處, 倒也無傷大雅。加上對照原文, 對於英文不好的我來說, 甚有幫助, 這是翻譯的價值。與其要求每個人把英文、日文、德文、法文學好, 才能吸收其中的專業知識, 倒不如用國家力量培養優秀的翻譯人員。



目录

前 言
第1章 构造过程抽象
1.1 程序设计的基本元素
1.2 过程与它们所产生的计算
1.3 用高阶函数做抽象
第2章 构造数据现象
2.1 数据抽象导引
2.2 层次性数据和闭包性质
2.3 符号数据
2.4 抽象数据的多重表示
2.5 带有通用型操作的系统
第3章 模块化、对象和状态
3.1 赋值和局部状态
3.2 求值的环境模型

3.3 用变动数据做模拟
3.4 并发:时间是一个本质问题
3.5 流
第4章 元语言抽象
4.1 元循环求值器
4.2 Scheme的变形——惰性求值
4.3 Scheme的变形——非确定性计算
4.4 逻辑程序设计
第5章 寄存器机器里的计算
5.1 寄存器机器的设计
5.2 一个寄存器机器模拟器
5.3 存储分配和废料收集
5.4 显式控制的求值器
5.5 编译

這是本怎麼樣的書呢? 主要在教如何寫程式, 以及程式是怎麼樣的一個東西。前兩章對一個有涉獵程式的人來說, 大概都能明白, 也就是一般 c++ 程式入門書籍上都會談到的概念。

資料抽象 - 將資料結構和操作的函數分開, 形成一個黑箱, 資料結構的改變不會影響這些操作函數, 將修改程度降到最低。這不是一般的 c++ 程式書籍都會提到的資料封裝嗎。

用不同的資料結構來表示同一個抽象資料, 書中提到複數的例子, 可以用直角座標或是極座標 (很久沒聽到這名詞了吧?) 來表示複數, 但也必須要在這兩種資料結構做轉換。都是表示複數, 沒道理處理直角座標的程式碼無法在極座標上執行, 該怎麼做呢? 最直觀就是提供轉換的函式, 這不也是在程式設計中會看到的技巧。c++ 可以實作 cast operator 和這個觀念很類似。

再來型別轉換一多的話, 程式碼會太複雜, 能辨識出型別的操作函數不是很棒, 怎麼做? 加個 tag 判斷即可。這不就是 if/switch/case 的應用嗎?

if circle do_circle
else do_others 

而 c++ 為了消除太多 if/switch/case 導入了 virtual function, 這邊也用了類似表格法來處理這樣的問題, 類似 c 的 function pointer。這些技巧都不會因為用什麼語言而有所不同, 是基本中的基本。

但從第三章開始, 就不是一般程式設計書籍提到的東西了, 你曾經想過變數與變數值的對應是怎麼實作的嗎? 在 script language 裡頭, 執行一個 expression 時會發生怎麼樣的行為呢? 函式的參數如何替換成真正的值? 3.2 有個概括的說明。

而第四章第五章的 metacircular evaluator, register base machine 更不是容易見到的主題。我實在很懷疑這樣的書籍竟然只是定位在入門等級。

從第一章看起 ...

1.1.1 談 expression
1.1.4 利用 define 來建立 procedure (如同 c function)

1.1.5 談到了 applicative order, normal order, p13 練習 1.5 有個有趣的題目:

1.5.scm
1 (define (p) (p))
2
3 (define (test x y)
4   (if (= x 0)
5     0
6     y))

當執行 (p) 會怎麼樣, (p) 會先去找出 p 是什麼, 結果又找到 (p), 然後又去找 p 是什麼 ... 永遠都找不到, 這就是 applicative order。
而 normal order, 就很順利執行完畢, 因為 normal order 不需要把 p 計算出來, 有需要的時候在計算即可。

在 mit-scheme 可以使用 delay 這個函數來執行 normal order, (delay (p)) 就不卡住了。

ref:

1.1.6 conditional expressions and predicate, 就是 if/else 用法。

這樣讀過來之後, 就可以開始用 scheme 寫程式了。不過和 c 語言不同, 沒有 for/while loop 這種東西, 那替代品是什麼呢? 是讓人傷透腦筋的 recursive。

1.1.7 從牛頓法開始求根號 X, 正常人應該都忘記什麼是牛頓法, 只記得牛頓是誰吧?

square_root.scm
 1 (define (sqrt-iter guess x)
 2   (if (good-enough? guess x)
 3     guess
 4     (sqrt-iter (improve guess x)
 5                x)))
 6 
 7 (define (improve guess x)
 8   (average guess (/ x guess)))
 9 
10 (define (average x y)
11   (/ (+ x y) 2))
12 
13 (define (good-enough? guess x)
14   (< (abs (- (square guess) x)) 0.001))
15 
16 (define (sqrt x)
17   (sqrt-iter 1.0 x))   
18 
19 (sqrt 2)

in mit-scheme
2 error> (load "square_root.scm")

;Loading "square_root.scm"... done
;Value: 1.4142156862745097

書上有寫的東西就不說明了, 這種語法, 還真是不習慣。

1.2.1 linear recursion and iteration, 介紹 recursive 和 iteration, 書中用了兩個 factorial 來解釋。

factorial.scm
 1 ; recursive
 2 (define (factorial n)
 3   (if (= n 1)
 4     1
 5     (* n (factorial (- n 1)))))
 6 (factorial 5)
 7 
 8 ; iteration
 9 (define (factorial n)
10   (fact-iter 1 1 n))
11   
12 (define (fact-iter product counter max-count)
13   (if (> counter max-count)
14     product
15     (fact-iter (* counter product)
16                (+ counter 1)
17                max-count)))

iteration 的版本看起來好像是 recursive, 但這和 c 語言的 while, for loop 是一樣的概念。

1.2.3 介紹計算所需的時間, 計算所需要的記憶體空間。

1.3.2 介紹 lambda, 不過我覺得 the roots of lisp 解釋的比較好。

(lambda (x) (+ x 4)) 這個是 function。

((lambda (x) (+ x 4)) 5) => 9 這叫 function call。
5 是 argument (引數), x 是 parameter (參數), (+ x 4) function body。

lambda 建立的是一個沒有名字的函數, 要使用這個函數就要用上述的那個語法。配合上 define, 就可以將這個 function 對應到一個名字。

(define plus4 (lambda (x) (+ x 4)))

也可以寫成

(define (plus4 x) (+ x 4))

這是一種語法糖衣。

let 也是一種 lambda 的語法糖衣。

page 42
(define (f x y)
((lambda (a b)
(+ (* x (square a))
(* y b)
(* a b)))
(+ 1 (* x y))
(- 1 y)))

(define (f x y)
(let ((a (+ 1 (* x y)))
(b (- 1 y)))
(+ (* x (square a))
(* y b)
(* a b))))

let 的功能, 可以由 lambda 來提供。

1.3.3 將一個 procedure 當作參數傳給一個 procedure

1.3.4 傳回一個 procedure, 這還真難懂, 花了我好些時間。

(define (average-damp f)
  (lambda (x) (average x (f x))))

真的很難懂吧!

first-class elements:

  • They may be named by variables.
  • They may be passed as arguments to procedures.
  • They may be returned as the results of procedures.
  • They may be included in data structures.65

符合這些條件的 function 就稱為 first-class function, 又是一個流行的名詞。當然符合這些條件的 object 就是 first-class object, 當然符合這些條件的 people 就是 first-class people XD

這是由 the British computer scientist Christopher Strachey (1916-1975) 幫我們整理出來的概念。



2.1.1 p56 介紹了 pair, 有相關的操作函數 cons, car, cdr, list (p66, 為了方便使用 cons, scheme 提供了 list) p57 最下面的註解解釋了 cons, car (Contents of Address part of Register, cdr (Contents fo Decrement part of Register) 這幾個奇怪的名稱。

2.1.3 p61 實作了 cons, car, cdr。

(define (cons x y)
(define (dispatch m)
(cond ((= m 0) x)
((= m 1) y)
(else (error "Argument not 0 or 1 -- CONS" m))))
dispatch)

(define (car z) (z 0))

(define (cdr z) (z 1))

(car (cons x y))
(cdr (cons x y))

cons 會傳回一個 procedure, 就是 dispatch, 再用來和 car, cdr 一起運作。

2.2.1 p67 提到 (cadr [arg]) = (car (cdr [arg])) 這樣的縮寫方式, 免得寫很多 car, cdr ...

2.2 Hierarchical Data and the Closure Property, 這個 Closure 不是你想的那種, 這是指這個資料結構可以一直的串下去。





cons 最主要就是一個 pair 結構, (cons 1 (cons 2 (cons 3 '() ))) 這樣的資料結構就是由這些 pair 一個一個串接起來, 別小看這麼簡單的東西, 這除了可以表達很直觀的 sequence 結構外, 還可以看做是一顆樹的結構。在 3.3.2 還會用這個來表示 queue, 用這樣的方式:



很難懂, 我花了一天才搞懂這個 queue 的表達方式。
ref: http://descent-incoming.blogspot.tw/2014/08/implement-queue-by-mit-scheme.html

2.3.3 以集合 (sets) 為範例, 以 list (排序和未排序兩種 list) 和 tree 為資料結構, 實作加入一個元素到集合裡頭, 某個元素是否在這個集合, 找出差集和聯集。

scheme 當然沒有類似 c 的 struct 這種東西, 書中用 list 來模擬 tree。

2.4.3 有提到一個 message passing 的設計方式。

3.1 這節在描述 assginment 和 local 變數的行為。
3.1.1 導入了 set! 用來做 assginment。

3.1.3 說明了 functional programming 和 imperative programming。

functional programming wiki 上的解釋
In computer science, functional programming is a programming paradigm, a style of building the structure and elements of computer programs, that treats computation as the evaluation of mathematical functions and avoids state and mutable data.

函數式編程(英語:Functional programming)或者函式程式設計,又稱泛函編程,是一種編程典範,它將電腦運算視為數學上的函式計算,並且避免使用程式狀態以及易變物件。

相信你看得一頭霧水。來看 sicp 3.1.1 的解釋:

sicp 3.1.3
Programming without any use of assignments, as we did throughout the first two chapters of this book, is accordingly known as functional programming.

函數式程式設計原來就是這樣定義, 之前都搞不懂。網路有些文章還寫像數學函數那樣來寫程式 (大概都是抄 wiki 的吧), 數學函式我知道, 但還是不知道什麼是函數式程式。看些討論講得好神, 不過基本就是這樣。而採用 assignments 的稱為 imperative programming。沒用 assignments 的就是函數式程式設計

不過我覺得書上提到的 assignment 比較像是使用 c 的 static variable 作 assignment 的動作。

難怪在這前兩章的範例程式時, 就覺得哪裡怪怪的, 沒錯, 我都沒看到 assignment 的概念。原來沒 assignment 也是可以寫程式的。

自制編程語言 page 7 寫的更直接: 「變數值無法被更改」的語言就是函數式編程語言。我覺得這更容易理解, 一句話道出 functional programdming 真諦。

factorial_1.scm functional programming
1 (define (factorial n)
2   (define (iter product counter)
3     (if (> counter n)
4       product
5       (iter (* counter product)
6             (+ counter 1))))
7     (iter 1 1))
8 
9 (factorial 5)

factorial_2.scm imperative programming
 1 (define (factorial n)
 2   (let ((product 1)
 3         (counter 1))
 4     (define (iter)
 5       (if (> counter n)
 6         product
 7         (begin (set! product (* counter product))
 8                (set! counter (+ counter 1))
 9                (iter))))
10     (iter)))
11 
12 (factorial 6)

上述程式碼便是分別展示這兩種編程方式。

3.2 在說明引進 assignment 之後, 整個程式的運作模型。很重要, 也很難理解, 我花了不少時間在這邊。

3.2.1 說明了當定義一個 procedure (在 scheme 只能用 lambda 產生一個 procedure), 或使用 define (這只是一個語法糖衣, 實際是用 lambda 產生一個 procedure) 產生一個 procedure 時, 怎麼將這個 procedure 和該名字關聯在一個環境裡。而這個 procedure object 包含兩部份, 一個是 procedure code 本身 (含型式參數部份), 一個是環境 (可能是一個指到某個環境的指標)。

在呼叫這個 procedure 時, 傳入的真正參數是怎麼和 procedure 的參數產生關聯, 這時後會建立一個新的環境/frame, 而呼叫這個 procedure 的環境便成為這個新環境的上層環境。

這節一開始我並沒有完全看懂, 而是看懂 4.1 metacircular evaluator source code 這部份的程式碼時, 我回頭看了好幾次才看懂。也因為 bind 被翻譯成約束, 有些句子也成為閱讀障礙, 建議對照原文, 應該會比較清楚。

雖然翻譯有些問題, 但我還是覺得中文版本幫上不少忙, 當然有原文對照會比較好些。若是沒有中文版本, 我閱讀的速度絕對不能這麼快。

這裡的解釋很重要, 是變數代換和 procedure 求值的關鍵, 若真看不懂, 只好看 4.1 metacircular evaluator source code。兩相對照, 相得益彰。

關鍵點有兩個:
  1. evaluate 一個 procedure 時, 建立 procedure object (一個 pair, 其中是 function body, 另一個是環境指標, 指到某個環境), 環境指標的上層環境是誰也要清楚。
  2. apply 一個 procedure 時, 環境是怎麼建立 (會建立一個新環境), 上層環境是誰? 如何將變數值對應到函式的參數。

(define (square x) (* x x)) 這是 evaluate
(square 5) 這是 apply

3.2.2, 3.2.3 3.2.4 以三個實際的程式碼, 分別說明了, 環境如何建立, 對應的變數如何與實際的變數值對應起來, 不算好懂, 得花點腦力才行。不過這個觀念很重要, 可以得知變數是怎麼關聯起來的, 為什麼同名稱的變數不會互相干擾, 獨立對應到不同的值。

要搞懂 interpreter, 3.2 這部份的東西要有清楚的認知。

3.2.3 的例子:

withdraw.scm
 1 (define (make-withdraw balance)
 2   (lambda (amount)
 3     (if (>= balance amount)
 4       (begin (set! balance (- balance amount))
 5              balance)
 6       "Insufficient funds")))
 7
 8 (define W1 (make-withdraw 100))
 9 (W1 50)
10 (W1 20)

說明環境怎麼用來維護 balance 這個局部變數, 有點像是 c 的 static 變數。

3.3.1 則介紹了 set-car!, set-cdr! 來改變一個 list。

change_list
 1 1 ]=> (define alist (list 1 2))
 2
 3 ;Value: alist
 4
 5 1 ]=> (display alist)
 6 (1 2)
 7 ;Unspecified return value
 8
 9 1 ]=> (set-car! alist 5)
10
11 ;Unspecified return value
12
13 1 ]=> (display alist)
14 (5 2)
15 ;Unspecified return value

3.4 concurrency: Time is of the essence 提出撰寫 concurrency 程式的問題, 以及利用 mutex 來處理共享資源。在目前這麼流行的 thread 環境中, 這些都是大家朗朗上口的概念, 但這本書可是很早就討論了這些東西, 而就算大家朗朗上口這些概念, 把 thread 程式寫好也不是件容易的事。

chapter4 是很精彩的一章, 4.1 要寫出一個 metacircular evaluator, 講白話就是用 lisp 寫出一個 lisp interpreter, 這裡的實作當然是 scheme。基本上只要讀到 4.1.4 就有個可以執行 scheme 的 interpreter, 是不是很興奮阿! 這可說是我讀這本書的最大動力了, 好久好久以前就想知道 interpreter 是怎麼做的。

如果你覺得 chapter 1, 2, 3 已經很難, 那我得告訴你, 這章更難, 得要有花更多時間的心理準備, 不過好消息是: 這章不是最難的, 後面那章更難。

我飛快閱讀過前面三章, 略過習題, 就是為了可以儘快看到這章。我把精力花在 4.1.1 ~ 4.1.4, 並以 c++ 實作了相同的版本。也許這樣講誇張了點, 光讀這四小結的時間可抵上 1, 2 章。

只要讀完這四個小節, 就可以寫出四則運算、自訂變數、函式呼叫、判斷式的直譯器, The unix programming environment 可是用了一章 (第八章) 在講這些東西, 應該是最簡單的文件了。不過我得先告訴你, 我可是花了至少一個月的時間才真的搞懂, 研究直譯器真的很有趣, 但真的很花時間、精力。

4.1.5

(eval '(* 5 5) user-initial-environment)
(eval (cons '* (list 5 5)) user-initial-environment)

eval 需要帶入一個環境

4.1.6 提到了 internal definitions, 看以下的程式碼:

(define (f x)
(define (even? n)
(if (= n 0)
true
(odd? (- n 1))))
(define (odd? n)
(if (= n 0)
false
(even? (- n 1))))
)

雖然在 even? 內用到 odd?, 但這應該難不倒你, 這個程式可以正常執行。

Exercise 4.19.

(let ((a 1))
(define (f x)
(define b (+ a x))
(define a 5)
(+ a b))
(f 10))

這個呢? (f 10) 會得到 20 還是 16 還是發出錯誤訊息? 難倒你了吧? 在 mit-scheme 是發出錯誤訊息, 我也喜歡這樣的實作方式。

4.4 引入了 logic programming, 我對這個主題很陌生, 有了 functional programming 還有個 logic programming, 4.4 給了一個例子說明什麼是 logic programming。不過你以為這樣就結束了嗎? 4.4.* 計劃要打造出一個 logic programming 的 interpreter, 哇 ... 一定要這麼猛嗎? 我快要投降了。

chapter 5 則更進一步, 打造一台模擬機器, 讓你在上頭跑 scheme 的程式。和 java 的 virtual machine (stack base) 不同, sicp 介紹的是 register base machine。這章也是要燃燒很多的腦力, 難度比第四章略難。

5.1 介紹了計算 GCD, factorial 的機器設計流程。

5.1.1 用了一個描述暫存器機器的語言, 如下:

gcd
1 (controller
2  test-b
3    (test (op =) (reg b) (const 0))
4    (branch (label gcd-done))
5    (assign t (op rem) (reg a) (reg b))
6    (assign a (reg b))
7    (assign b (reg t))
8    (goto (label test-b))
9  gcd-done)

這是用來描述一個計算 GCD 的機器, 只要把暫存器 a, b 存入兩個數字, 機器運算後, 會在暫存器 a 得到這兩個數的 GCD。

5.1.4 提到使用 stack 來實作 recursive。用了 gcd, factorial 來說明這兩個 recursive 的不同。

gcd 只是很單純將原來的問題簡化為另外的 gcd 計算; 而 factorial 則需要計算出另外的 (n-1)!, 並需要這個 (n-1) 的結果。gcd 的計算不需要用上 stack 來支援 recursive, factorial 則需要 stack 來支援 recursive。這個小節最後還以 fibonacci 來說明 double recursion 的概念。

5.2 實作模擬器程式碼, 跑一下哪個 GCD 機器。程式碼難到嚇人, 我這 scheme 初學者完敗在 5.2.2 的 extract-labels, recursive callback function 真的難倒我。

看懂這段程式碼後, 我寫了一篇文章: recursive 加上 callback function

透過程式碼了解其基本精神後, 我大概知道要怎麼寫一個類似 qemu 的模擬器了。

5.4 將寫出一個 explicit-control evaluator, 用來執行 4.1 的那個 metacircular evaluator, 真是太酷了!

有一個 scheme chip, 可參考:
Batali, J. et al. "The Scheme-81 Architecture—System and Chip." Proc. Conference on Advanced Research in VLSI, P. Penfield Jr. ed., MIT, 1982, pp. 69-77.
不過我找不到文章內容。

5.5 提到編譯這個主題, 和 5.4 的 interpreter 對照, 兩個不同的執行方式, 可以體會其中的優缺點。

5.5.7 會示範如何將 compile 後的 code 載入到模擬的機器執行。

這章一樣是要花費很大的時間、腦力, 本科系的朋友應該會很喜歡這些主題。

網路上有很多本書的討論訊息, 但如果你沒看過這本書, 應該還是不知道這本書在說什麼 (我認為是他們沒有完全讀完這本書, 再強調一次, 本書我覺得不好讀, 相當接近我讀的 os 書籍難度; 如果你聽過程式設計師的自我修養 - 連結、載入、程式庫這本書, 我覺得 sicp 比這本還難讀, 自我修養我覺得不算難讀), 希望這篇文章能讓你了解個大概, 也能吸引你去讀這本書。當然沒有哪本書是一定非讀不可, 你總是可以從其他地方或取相同的知識, 要得到本書的知識也並非要從本書開始。網路評價很好的書籍也並非就一定要讀。

這兩篇評論寫的很好, 一看就知道是有看過整本書的人所寫:

1996 的書過時了嗎? 我不認為, 光是第四章、第五章就值回票價。你去哪找兩章就可以講解/完成一個 interpreter 和 register base machine 的書。

相關資料

nil cannot work: http://stackoverflow.com/questions/9115703/null-value-in-mit-scheme
使用 '() 來代替 nil

ref:
  1. SICP读书笔记 (1) : http://kidneyball.iteye.com/blog/922953
  2. 老赵书托 (2): 计算机程序的构造与解释
  3. Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I by John McCarthy:
    htmlpdfpdf2.
  4. sicp code: http://mitpress.mit.edu/sicp/code/index.html
  5. 有一段 Alan J. Perlis 文章的翻譯。
  6. SICP in Python
  7. SICP 解题集
  8. sicp epub version
  9. 線上課程
簡體中文字幕 sicp 課程:
http://v.youku.com/v_show/id_XNTMxODY1NTg4.html

這是課程的其中一部分, 有興趣的朋友可自行搜尋其他部份。




2014年11月18日 星期二

array and pointer X global and local

我很想知道 g.c 和 g1.c 有什麼不同, source code 之前沒有秘密, 尤其是在組合語言之前。

先來四個問題 ...

Q1: g.c L3 可讀可寫, 為什麼?
Q2: g.c L4 read only, why?

g.c
1 int main(int argc, char *argv[])
2 {
3   char ch[]="abc";
4   const char *chp="abc";
5   return 0;
6 }

Q3: g1.c L1 可讀可寫, 為什麼?
Q4: g1.c L2 read only, why?

g1.c
1   char ch[]="abc";
2   const char *chp="abc";
3 int main(int argc, char *argv[])
4 {
5   ch[0] = '1';
6   return 0;
7 }

先來反組譯 g (source code g.c), L5 把 "abc" 複製到 ch, L6 把 chp 指到 "abc", 這個 "abc" 是為於 .rodata, 所以 chp 指到的內容不能改, chp 本身在 stack。

ch 本身位於 stack, 把 "abc" 複製到這裡自然是可以修改的。

objdump -d g
1 080483cd <main>:
2  80483cd:       55                      push   %ebp
3  80483ce:       89 e5                   mov    %esp,%ebp
4  80483d0:       83 ec 10                sub    $0x10,%esp
5  80483d3:       c7 45 f8 61 62 63 00    movl   $0x636261,-0x8(%ebp)
6  80483da:       c7 45 fc 80 84 04 08    movl   $0x8048480,-0x4(%ebp)
7  80483e1:       b8 00 00 00 00          mov    $0x0,%eax
8  80483e6:       c9                      leave  
9  80483e7:       c3                      ret   

g1呢? 不用反組譯, 看他的 .s 就可以了。ch 和 ch 所指到的 "abc" 在 .data section, 所以可讀寫。 chp 本身位於 .data, 可讀可寫, 但它指到的 "abc" 位於 .rodata, 只能讀取。

gcc -S g1.c
 1  .file "g1.c"
 2  .globl ch
 3  .data
 4  .type ch, @object
 5  .size ch, 4
 6 ch:
 7  .string "abc"
 8  .globl chp
 9  .section .rodata
10 .LC0:
11  .string "abc"
12  .data
13  .align 4
14  .type chp, @object
15  .size chp, 4
16 chp:
17  .long .LC0
18  .text
19  .globl main
20  .type main, @function
21 main:
22 .LFB0:
23  .cfi_startproc
24  pushl %ebp
25  .cfi_def_cfa_offset 8
26  .cfi_offset 5, -8
27  movl %esp, %ebp
28  .cfi_def_cfa_register 5
29  movb $49, ch
30  movl $0, %eax
31  popl %ebp
32  .cfi_restore 5
33  .cfi_def_cfa 4, 4
34  ret
35  .cfi_endproc
36 .LFE0:
37  .size main, .-main
38  .ident "GCC: (Debian 4.8.2-16) 4.8.2"
39  .section .note.GNU-stack,"",@progbits

.rodata section 為什麼會有 read only 的能力, 那是因為 mmu 的作用, 所以若沒有這種記憶體保護機制, 是可以直接修改值的。例如: dos。

就像蒙面魔術師破解魔術一樣, 了解之後, 就知道為什麼可以這樣, 不能那樣。

組合語言的聽說讀寫, 最好能有的能力, 這樣才能了解最底層的秘密。

2014年11月14日 星期五

作業系統之前的程式 for stm32f4discovery (10.2) - 印出浮點數, 使用硬體浮點運算器

為什麼這篇 [作業系統之前的程式 for stm32f4discovery (10) - 印出浮點數] 要用軟體浮點運算函式庫而不用硬體呢?

A: 因為我那時候還不會用, 不過現在我會了, 趕緊來介紹使用硬體浮點運算器的版本。

我當時以為只要使用 -mfloat-abi=hard 來編譯程式就可以, 不過還需要設定浮點運算 coprocessor 的暫存器, L491, L492 就是在做這樣的事情。

L490 是 arm 的官方連結。

float_hard.cpp
486 
487 void WinMain()
488 {
489   // enable vfp
490   // ref: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0439d/BEHBJHIG.html
491   u32 CPACR_REG_ADDR = 0xE000ED88;
492   *(volatile u32*)CPACR_REG_ADDR = ((3UL << 10*2)|(3UL << 11*2));
493 
494   //SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));
495 
496   init_usart(9600);
497   //init_command();
498 
499   ur_puts(USART2, "Init complete! Hello World!\r\n");
500   float f=3.275;
501   myprint("\r\nabc\r\n");
502   myprint_float(f);
503   myprint("\r\nend\r\n");
504 
505   while(1)
506   {
507   }
508 }

下面兩個反組譯的程式碼列出其中的不同。

float.dis
 934 080007d8 <WinMain>:
 935  80007d8: b580       push {r7, lr}
 936  80007da: b082       sub sp, #8
 937  80007dc: af00       add r7, sp, #0
 938  80007de: f44f 5016  mov.w r0, #9600 ; 0x2580
 939  80007e2: f7ff ff31  bl 8000648 <_Z10init_usartm>
 940  80007e6: 4808       ldr r0, [pc, #32] ; (8000808 <WinMain+0x30>)
 941  80007e8: 4908       ldr r1, [pc, #32] ; (800080c <WinMain+0x34>)
 942  80007ea: f7ff ff83  bl 80006f4 <_Z7ur_putsP13USART_TypeDefPKc>
 943  80007ee: 4b08       ldr r3, [pc, #32] ; (8000810 <WinMain+0x38>)
 944  80007f0: 607b       str r3, [r7, #4]
 945  80007f2: 4808       ldr r0, [pc, #32] ; (8000814 <WinMain+0x3c>)
 946  80007f4: f000 f99c  bl 8000b30 <_Z7myprintPKc>
 947  80007f8: 6878       ldr r0, [r7, #4]
 948  80007fa: f000 f9c3  bl 8000b84 <_Z13myprint_floatf>
 949  80007fe: 4806       ldr r0, [pc, #24] ; (8000818 <WinMain+0x40>)
 950  8000800: f000 f996  bl 8000b30 <_Z7myprintPKc>
 951  8000804: e7fe       b.n 8000804 <WinMain+0x2c>
 952  8000806: bf00       nop
 953  8000808: 40004400  .word 0x40004400
 954  800080c: 080011c8  .word 0x080011c8
 955  8000810: 4051999a  .word 0x4051999a
 956  8000814: 080011e8  .word 0x080011e8
 957  8000818: 080011f0  .word 0x080011f0
 958 

硬體的版本使用了 vldr 這類指令。

float_hard.dis
   1 
 934 080007d8 <WinMain>:
 935  80007d8: b580       push {r7, lr}
 936  80007da: b082       sub sp, #8
 937  80007dc: af00       add r7, sp, #0
 938  80007de: 4b0d       ldr r3, [pc, #52] ; (8000814 <WinMain+0x3c>)
 939  80007e0: 607b       str r3, [r7, #4]
 940  80007e2: 687b       ldr r3, [r7, #4]
 941  80007e4: f44f 0270  mov.w r2, #15728640 ; 0xf00000
 942  80007e8: 601a       str r2, [r3, #0]
 943  80007ea: f44f 5016  mov.w r0, #9600 ; 0x2580
 944  80007ee: f7ff ff2b  bl 8000648 <_Z10init_usartm>
 945  80007f2: 4809       ldr r0, [pc, #36] ; (8000818 <WinMain+0x40>)
 946  80007f4: 4909       ldr r1, [pc, #36] ; (800081c <WinMain+0x44>)
 947  80007f6: f7ff ff7d  bl 80006f4 <_Z7ur_putsP13USART_TypeDefPKc>
 948  80007fa: 4b09       ldr r3, [pc, #36] ; (8000820 <WinMain+0x48>)
 949  80007fc: 603b       str r3, [r7, #0]
 950  80007fe: 4809       ldr r0, [pc, #36] ; (8000824 <WinMain+0x4c>)
 951  8000800: f000 f9a6  bl 8000b50 <_Z7myprintPKc>
 952  8000804: ed97 0a00  vldr s0, [r7]
 953  8000808: f000 f9cc  bl 8000ba4 <_Z13myprint_floatf>
 954  800080c: 4806       ldr r0, [pc, #24] ; (8000828 <WinMain+0x50>)
 955  800080e: f000 f99f  bl 8000b50 <_Z7myprintPKc>
 956  8000812: e7fe       b.n 8000812 <WinMain+0x3a>
 957  8000814: e000ed88  .word 0xe000ed88
 958  8000818: 40004400  .word 0x40004400
 959  800081c: 08000bf8  .word 0x08000bf8
 960  8000820: 4051999a  .word 0x4051999a
 961  8000824: 08000c18  .word 0x08000c18
 962  8000828: 08000c20  .word 0x08000c20
 963 

toolchain version
descent@debianlinux:float$ arm-none-eabi-g++ --version
arm-none-eabi-g++ (GCC) 4.9.1
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

stm32f4discovery 在 minicom 上的執行畫面:
Init complete! Hello World!
abc
3.27500
end

source code:
https://github.com/descent/stm32f4_prog/tree/master/float

ref: ARM Cortex-M4嵌入式实战开发精解:基于STM32F4 chapter 6

2014年11月12日 星期三

linux linked list

the 1st edition: 20131017
the 2nd edition: 20141112

我不確定是不是和 linux list 一樣, 這是一步步写嵌入式操作系统:ARM编程的方法与实践提到的 list 資料結構, 應該是從 linux 那個取經而來。有點複雜, 花了點腦力才搞懂。

基本原理:
以 list.c L7 struct page 來說, 只要知道 list 成員這個位址, 就可以計算出這個 struct page 的位址, 假設 unsigned int/int 佔 4 byte, list 位址是 28, 那這個 struct page 的位址是 28 - 4 * 3 = 16, 就是這麼簡單, 但是那些計算的 macro 就不簡單了。

我並不是要說明 list_entry macro, 書上解說的很詳細 (p148~154, 8 頁的說明, 我怎麼可能在 blog 解釋的比書上還好呢), 但我還是沒能理解, 寫個小範例來配合書上的說明。

如果只是要使用這個 function, 那就簡單多了, 參考 Linux kernel linked list for user space

list.c
  1 #include <stdio.h>
  2 
  3 struct list_head {
  4  struct list_head *next, *prev;
  5 };
  6 
  7 struct page {
  8  unsigned int vaddr;
  9  unsigned int flags;
 10  int order;
 11  //struct kmem_cache *cachep;
 12  struct list_head list;//to string the buddy member
 13 };
 14 
 15 
 16 static inline void INIT_LIST_HEAD(struct list_head *list)
 17 {
 18  list->next = list;
 19  list->prev = list;
 20 }
 21 
 22 static inline void __list_add(struct list_head *new_lst,
 23          struct list_head *prev,
 24          struct list_head *next)
 25 {
 26  next->prev = new_lst;
 27  new_lst->next = next;
 28  new_lst->prev = prev;
 29  prev->next = new_lst;
 30 }
 31 
 32 static inline void list_add(struct list_head *new_lst, struct list_head *head)
 33 {
 34  __list_add(new_lst, head, head->next);
 35 }
 36 
 37 static inline void list_add_tail(struct list_head *new_lst, struct list_head *head)
 38 {
 39  __list_add(new_lst, head->prev, head);
 40 }
 41 
 42 static inline void __list_del(struct list_head * prev, struct list_head * next)
 43 {
 44  next->prev = prev;
 45  prev->next = next;
 46 }
 47 
 48 static inline void list_del(struct list_head * entry)
 49 {
 50  __list_del(entry->prev,entry->next);
 51 }
 52 
 53 
 54 static inline void list_remove_chain(struct list_head *ch,struct list_head *ct){
 55  ch->prev->next=ct->next;
 56  ct->next->prev=ch->prev;
 57 }
 58 
 59 static inline void list_add_chain(struct list_head *ch,struct list_head *ct,struct list_head *head){
 60   ch->prev=head;
 61   ct->next=head->next;
 62   head->next->prev=ct;
 63   head->next=ch;
 64 }
 65 
 66 static inline void list_add_chain_tail(struct list_head *ch,struct list_head *ct,struct list_head *head){
 67   ch->prev=head->prev;
 68   head->prev->next=ch;
 69   head->prev=ct;
 70   ct->next=head;
 71 }
 72 
 73 
 74 static inline int list_empty(const struct list_head *head)
 75 {
 76  return head->next == head;
 77 }
 78 
 79 
 80 #define offsetof(TYPE, MEMBER) ((unsigned int) &((TYPE *)0)->MEMBER)
 81 
 82 
 83 #define container_of(ptr, type, member) ({   \
 84  const typeof( ((type *)0)->member ) *__mptr = (ptr); \
 85   (type *)( (char *)__mptr - offsetof(type,member) );})
 86 
 87 
 88 #define list_entry(ptr,type,member) \
 89     container_of(ptr, type, member)
 90 
 91 
 92 
 93 #define list_for_each(pos, head) \
 94  for (pos = (head)->next; pos != (head); pos = pos->next)
 95 
 96 
 97 int main(int argc, const char *argv[])
 98 {
 99   struct page p1, p2;
100 
101   p1.list.next = p2.list.prev;
102   struct list_head *lh = &p1.list;
103   //struct list_head *lh = &p1.list;
104 
105   struct page *p3=0;
106   ((struct page *)0)->list ;
107   printf("%p\n", &((struct page *)0)->list);
108   printf("%p\n", (char *)lh);
109   printf("%p\n", ( (char *)lh - ((unsigned int) &((struct page *)0)->list) ) );
110   printf("%p\n", &p1);
111         
112   //list_entry(lh,struct page,list);
113   return 0;
114 }

200   ({ const typeof( ((struct page *)0)->list ) *__mptr = (lh); 
201   (struct page *)( (char *)__mptr - ((unsigned int) &((struct page *)0)->list) );});
202   return 0;

L80 ~ L89 真是令人害怕的 macro, 還有一個不認識的 typeof, 不認識很正常, 這是 gcc 的 c extension, 很討厭這種 extension 吧, 難道沒用上 extension 就做不到同樣的事情嗎? 當然不是, 只是選擇不同。

L112 展開是 L200, L201 的結果 (還是讓人害怕, 書上用了 8 頁的說明現在你覺得有道理了吧), 我分別從 L105~110 做了單獨的測試。最主要是要補充書上沒提到的小例子, 有了這個小例子, 搭配書上的講解 (6.1.2.2 page 148~154), 應該就能了解。

我沒注意到 ({}) 這用法, 這也是 gcc extension:
http://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html
http://www.ptt.cc/bbs/C_and_CPP/M.1383271390.A.B23.html

一開始被 0 取址所迷惑, 以為這樣會 core dump, 不過應該是要做寫入的動作才會 core dump, 讀取位址 0 是沒問題的。有點問題, 在我的 linux 環境下, 就算只是讀取位址 0 的內容也是有問題的。這裡應該要這樣說: &((struct page *)0)->list) 把這個 list 的位址取出來是沒有問題的, 縱使是用 address 0 去強制轉型出來。

struct list_head addr = (((struct page *)0)->list);
有問題

struct list_head *addr = &(((struct page *)0)->list);
沒問題

一個更短的例子:
zero.c
1 int main(int argc, char *argv[])
2 {
3   unsigned int i;
4
5   i = * (volatile unsigned int *)0;
6   return 0;
7 }

這在我的 linux 下會得到 Segmentation fault。

z.disasm
144 080483cc <main>:
145  80483cc: 55                    push   %ebp
146  80483cd: 89 e5                 mov    %esp,%ebp
147  80483cf: 83 ec 10              sub    $0x10,%esp
148  80483d2: b8 00 00 00 00        mov    $0x0,%eax
149  80483d7: 8b 00                 mov    (%eax),%eax
150  80483d9: 89 45 fc              mov    %eax,-0x4(%ebp)
151  80483dc: b8 00 00 00 00        mov    $0x0,%eax
152  80483e1: c9                    leave  
153  80483e2: c3                    ret    
154  80483e3: 66 90                 xchg   %ax,%ax
155  80483e5: 66 90                 xchg   %ax,%ax
156  80483e7: 66 90                 xchg   %ax,%ax
157  80483e9: 66 90                 xchg   %ax,%ax
158  80483eb: 66 90                 xchg   %ax,%ax
159  80483ed: 66 90                 xchg   %ax,%ax
160  80483ef: 90                  

執行到 L149 會產生 Segmentation fault, 看來 linux 把讀取 address 0 也當作是一個 memory access exception。

descent@debian-vm:part2$ ./list 
0xc
0xbfe84500
0xbfe844f4
0xbfe844f4

真是厲害, 我看過的版本是使用 c macro, 造出類似 c++ template 的技巧; c++ 有 template, 就不用這麼轉腦袋了, 當然一樣有型別檢查, 而且和課本的內容像多了。下面的程式碼是 c++ template 的版本。

list.cpp
 1 // template linked list test in os kernel
 2 #include <cstdio>
 3 
 4 using namespace std;
 5 
 6 #include "stm32.h"
 7 
 8 template <typename type>
 9 class List
10 {
11   public:
12     List(type *item)
13     {
14       set_item(item);
15       prev_ = next_ = 0;
16     }
17     void add_next(List *n)
18     {
19       next_ = n;
20     }
21     type *item(){return item_;}
22     void set_item(type *item)
23     {
24       item_ = item;
25     }
26     List *next(){return next_;}
27   private:
28     type *item_;
29     List *prev_;
30     List *next_;
31 };
32 
33 struct page 
34 {
35  unsigned int vaddr;
36  unsigned int flags;
37  int order;
38 };
39 
40 
41 void mymain()
42 {
43   page p1, p2;
44   List<page> node1(&p1), node2(&p2);
45   List<page> *head;
46 
47 #if 0
48   printf("%p\n", &p1);
49   printf("%p\n", &p2);
50 #endif
51 
52   //node1.set_item(p1);
53   //node2.set_item(p2);
54 
55   node1.add_next(&node2);
56   head = &node1;
57   page *cur_page = 0;
58   for (List<page> *cur=head ; cur ; cur=cur->next())
59   {
60     cur_page =  cur->item();
61     //printf("%p\n", cur->item());
62   }
63   while(1);
64 
65 #if 0
66 
67   p1.list.next = p2.list.prev;
68   struct list_head *lh = &amp;p1.list;
69   //struct list_head *lh = &amp;p1.list;
70 
71   struct page *p3=0;
72   ((struct page *)0)-&gt;list ;
73   printf("%p\n", &amp;((struct page *)0)-&gt;list);
74   printf("%p\n", (char *)lh);
75   printf("%p\n", ( (char *)lh - ((unsigned int) &amp;((struct page *)0)-&gt;list) ) );
76   printf("%p\n", &amp;p1);
77         
78   //list_entry(lh,struct page,list);
79   return 0;
80 #endif
81 }
82 
83 #if 0
84   ({ const typeof( ((struct page *)0)-&gt;list ) *__mptr = (lh); 
85   (struct page *)( (char *)__mptr - ((unsigned int) &amp;((struct page *)0)-&gt;list) );});
86   return 0;
87 #endif

跑在 os 下使用這功能沒什麼特別的, 這是要在 os 層級使用的, 來試試看能否成功。

arm-none-eabi-g++ -fno-common -O0 -g -mcpu=cortex-m3 -mthumb  -fno-exceptions -fno-rtti -std=c++11  -c list.cpp 
arm-none-eabi-g++ -Wl,-T./stm32.ld -nostartfiles -fno-common -mcpu=cortex-m3 -mthumb -o list.elf list.o
arm-none-eabi-objcopy -O binary list.elf list.bin

qemu-system-arm -M lm3s6965evb -kernel list.bin -S -gdb tcp::1234

為了簡單, 就不要出動列印功能 (不使用 printf), 直接透過 qemu/gdb 來檢視正確性, gdb 可觀察到 p1, p2 位址, 和使用 List 取出的是一樣的。藉由 template, 可以塞進各種型別, 相信 c++ template 版本看來友善多了。

gdb
 1 (gdb) target remote :1234
 2 Remote debugging using :1234
 3 ResetISR () at stm32.h:21
 4 21      {
 5 (gdb) b mymain
 6 Breakpoint 1 at 0x10e: file list.cpp, line 44.
 7 (gdb) c
 8 Continuing.
 9 
10 Breakpoint 1, mymain () at list.cpp:44
11 44        List<page> node1(&p1), node2(&p2);
12 (gdb) p &p1
13 $1 = (page *) 0x200000d0
14 (gdb) p &p2
15 $2 = (page *) 0x200000c4
16 (gdb) n
17 55        node1.add_next(&node2);
18 (gdb) n
19 56        head = &node1;
20 (gdb) n
21 57        page *cur_page = 0;
22 (gdb) n
23 58        for (List<page> *cur=head ; cur ; cur=cur->next())
24 (gdb) n
25 60          cur_page =  cur->item();
26 (gdb) p cur_page 
27 $3 = (page *) 0x0 <VectorTable>
28 (gdb) n
29 58        for (List<page> *cur=head ; cur ; cur=cur->next())
30 (gdb) p cur_page 
31 $4 = (page *) 0x200000d0
32 (gdb) n
33 60          cur_page =  cur->item();
34 (gdb) n
35 58        for (List<page> *cur=head ; cur ; cur=cur->next())
36 (gdb) p cur_page 
37 $5 = (page *) 0x200000c4
38 (gdb) 

L13, L15, 是 p1, p2 的位址, L31, L37 是透過 List 取出的位址, 剛好一樣 (不一樣就慘了)。

以下的 gdb 環境為 stm32f4 discovery board, 和模擬器的值一模一樣。

gdb stm32f4 discovery board
 1 $1 = {vaddr = 3204496768, flags = 134218252, order = 536870912}
 2 (gdb) p &p1
 3 $2 = (page *) 0x200000d0
 4 (gdb) p &p2
 5 $3 = (page *) 0x200000c4
 6 (gdb) n
 7 55   node1.add_next(&node2);
 8 (gdb) n
 9 56   head = &node1;
10 (gdb) n
11 57   page *cur_page = 0;
12 (gdb) n
13 58   for (List<page> *cur=head ; cur ; cur=cur->next())
14 (gdb) n
15 p 60     cur_page =  cur->item();
16 (gdb) p cur_page
17 $4 = (page *) 0x0
18 (gdb) n
19 58   for (List<page> *cur=head ; cur ; cur=cur->next())
20 (gdb) p cur_page
21 $5 = (page *) 0x200000d0
22 (gdb) n
23 60     cur_page =  cur->item();
24 (gdb) n
25 58   for (List<page> *cur=head ; cur ; cur=cur->next())
26 (gdb) p cur_page
27 $6 = (page *) 0x200000c4
28 (gdb) n
29 63   while(1);
30 (gdb) n
31 ^Cmymain () at list.cpp:63
32 63   while(1);
33 Error while handling inferior event:
34 Quit
35 mymain () at list.cpp:63
36 63   while(1);

c macro 版本莫測高深, c++ template 版本高深莫測, 相信每個人都有自己喜歡的作法, 神的右手與魔鬼的左手同樣的強大。使用 c++, 你可選神的右手或是魔鬼的左手, 這是我喜歡 c++ 的原因之一。

大概被 llvm 嚇到了, gcc 開始接受 c++ 了。

2014年11月7日 星期五

作業系統之前的程式 for stm32f4discovery (10) - 印出浮點數

漢兒學得胡兒語, 爭上牆頭罵漢人

終於來到 10 了, 這是目前最長的系列, 真是長壽。浮點數原來是這麼麻煩的東西, 這篇並不是要介紹浮點數的格式、原理, 而是怎麼把浮點數印出來。

printf("%f") 就搞定了, 你這麼說: 不過標題是作業系統之前的程式, 所以我們得自己提供這個功能, 當然不用把 printf 做出來, 先把 float 轉成 C 字串就可以了。

先來個提外話:

我在 p103 模擬器上測試程式碼時, 使用不同版本的 toolchain, 得到了以下的錯誤訊息, p103 的範例有很多是我不能掌握的程式碼, 單純是為了方便而使用 p103 模擬器。

/tmp/ccRko2hC.s: Assembler messages:
/tmp/ccRko2hC.s:813: Error: registers may not be the same -- `strexb r3,r2,[r3]'
/tmp/ccRko2hC.s:862: Error: registers may not be the same -- `strexh r3,r2,[r3]'
make: *** [main.elf] Error 1

若你遇到這問題, 請參考 Fix “registers may not be the same” ARM GCC error

core_cm3.diff
 1 diff --git a/libraries/CMSIS/CM3/CoreSupport/core_cm3.c b/libraries/CMSIS/CM3/CoreSupport/core_cm3.c
 2 index 56fddc5..0e8c3c4 100644
 3 --- a/libraries/CMSIS/CM3/CoreSupport/core_cm3.c
 4 +++ b/libraries/CMSIS/CM3/CoreSupport/core_cm3.c
 5 @@ -733,7 +733,7 @@ uint32_t __STREXB(uint8_t value, uint8_t *addr)
 6  {
 7     uint32_t result=0;
 8    
 9 -   __ASM volatile ("strexb %0, %2, [%1]" : "=r" (result) : "r" (addr), "r" (value) );
10 +   __ASM volatile ("strexb %0, %2, [%1]" : "=&r" (result) : "r" (addr), "r" (value) );
11     return(result);
12  }
13  
14 @@ -750,7 +750,7 @@ uint32_t __STREXH(uint16_t value, uint16_t *addr)
15  {
16     uint32_t result=0;
17    
18 -   __ASM volatile ("strexh %0, %2, [%1]" : "=r" (result) : "r" (addr), "r" (value) );
19 +   __ASM volatile ("strexh %0, %2, [%1]" : "=&r" (result) : "r" (addr), "r" (value) );
20     return(result);
21  }
22  

大概是這樣修正, 我不知道為什麼用了不同的 toolchain 會這樣, 至於 & 是表示被修飾的操作符只能作為輸出。

回到浮點數。

ftoa.cpp 從 How to implement “ char * ftoa(float num) ” without sprintf() library function in C, C++ and JAVA? 抄來的, 我做了一些修改, 避免使用 math.h 的東西, 拿掉 log10 和 pow 這兩個 function, 作業系統之前的程式都要自己來, 儘量不要用現成的 library; 作業系統之下的版本當然就直接 call log10 和 pow。這是面試題目, 多了解不會吃虧的。

float.cpp
217 static inline int mypow(int x, int y)
218 {
219   int r=1;
220   for (int i=0 ; i < y ; ++i)
221     r*=x;
222   return r;
223 }
224 
225 const int PRECISION = 6;
226 
227 static inline u8* float_to_str(float num)
228 {
229    int whole_part = num;
230    int digit = 0, reminder =0;
231    //int log_value = log10(num), 
232    int index;
233    float wt = 0.0;
234 
235    // String containg result
236    static u8 str[30];
237 
238    //Initilise stirng to zero
239    //memset(str, 0 ,20);
240    
241    //u8 whole_str[20] ;
242    s32_itoa(whole_part, str, 10);
243 #if 0
244    //Extract the whole part from float num
245    for(int  i = 1 ; i < log_value + 2 ; i++)
246    {
247        wt  =  mypow(10,i);
248        reminder = whole_part  %  wt;
249        digit = (reminder - digit) / (wt/10);
250 
251        //Store digit in string
252        str[index--] = digit + 48;              // ASCII value of digit  = digit + 48
253        if (index == -1)
254           break;    
255    }
256 #endif
257     //index = log_value + 1;
258     index = s_strlen((const char*)str);
259     str[index] = '.';
260 
261    float fraction_part  = num - whole_part;
262    float tmp1 = fraction_part,  tmp =0;
263 
264 #if 0
265    cout << "fraction_part: " << fraction_part << endl;
266    cout << "num: " << num << endl;
267    cout << "whole_part: " << whole_part << endl;
268 #endif
269    //Extract the fraction part from  num
270    for( int i= 1; i < PRECISION; i++)
271    {
272       wt =10; 
273       tmp  = tmp1 * wt;
274       digit = tmp;
275       //cout << "digit: " << digit << endl;
276       //cout << "tmp: " << tmp << endl;
277       //cout << "tmp1: " << tmp1 << endl;
278 
279       //Store digit in string
280       str[++index] = digit +48;           // ASCII value of digit  = digit + 48
281       tmp1 = tmp - digit;
282       if (tmp1 == 0.0)
283         break;
284    }    
285    str[index+1] = 0;
286 
287    return str;
288 }
289 
290 
291 #endif

這個演算法的重點有兩個:
  1. c 語言型別轉換知識 (這你不可能想得到, 只有靠閱讀 c 語言資料才會知道)
  2. 演算法 (你可以想出來怎麼做)
c 怎麼把浮點數轉成整數?
How do I round numbers?
C's floating to integer conversion truncates (discards) the fractional part
這是知識。

怎麼把數字 123 轉成 c 字串的 123, 這是演算法, 缺一不可。程式員真辛苦, 除了讀書還要思考, 可不是嘴炮般的說些自己想像的東西。

int i = 1.35f; 浮點數 1.35 會轉成整數 1 存給 i, 再用 1.35 - 1 得到 0.35, 有了 1 和 0.35, 想辦法把他們變成 char 存到 c 字串就搞定。

請參考 float.cpp L242 把整數 1 轉成字串, L207 把 0.35 轉成字元 3, 5, 最後在他們中間加上 "." 就搞定了。

int main(void)
{
  init_rs232();
  USART_Cmd(USART2, ENABLE);
  
  float f=1.5;
  myprint_float(f);
  myprint("\r\nabc");
  while(1);
}

還沒完, 怎麼 compile 浮點運算的程式碼可是個問題, 目標放在使用軟體的浮點運算函式庫 - libgcc, 所以要加上 -mfloat-abi=soft, link 時需要加上 -lgcc (arm-none-eabi-gcc -print-libgcc-file-name 可以顯示 libgcc 的路徑), 才剛說了大話馬上被打臉, 我還是用上了 gcc 裡頭的浮點運算函式庫。

我不是想偷懶, 只是神功還沒練成, 秘笈在這:



k_stdio.o: In function `float_to_str':
/home/descent/git/stm32_p103_demos/demos/uart_echo/k_string.h:229: undefined reference to `__aeabi_f2iz'
/home/descent/git/stm32_p103_demos/demos/uart_echo/k_string.h:261: undefined reference to `__aeabi_i2f'
/home/descent/git/stm32_p103_demos/demos/uart_echo/k_string.h:261: undefined reference to `__aeabi_fsub'
/home/descent/git/stm32_p103_demos/demos/uart_echo/k_string.h:273: undefined reference to `__aeabi_fmul'
/home/descent/git/stm32_p103_demos/demos/uart_echo/k_string.h:274: undefined reference to `__aeabi_f2iz'
/home/descent/git/stm32_p103_demos/demos/uart_echo/k_string.h:281: undefined reference to `__aeabi_i2f'
/home/descent/git/stm32_p103_demos/demos/uart_echo/k_string.h:281: undefined reference to `__aeabi_fsub'
/home/descent/git/stm32_p103_demos/demos/uart_echo/k_string.h:282: undefined reference to `__aeabi_fcmpeq'

如果你遇上這些連結錯誤的話, 可能是忘了加上 -lgcc, 很奇怪, 有的 toolchain 不需要明確指出 -lgcc, 有的卻需要, libgcc 明明是 gcc 內建的, 怎麼會需要特別指定呢? 奇怪。這是我在 p103 模擬器上的測試程式碼, p103 的範例有很多是我不能掌握的 code, 是我純粹為了方便測試為主。你可能發現前面講過了, 因為是從這裡複製貼上修改到前面的。

模擬器執行結果
 1 descent@debianlinux:uart_echo$ /media/2/qemu_stm32/arm-softmmu/qemu-system-arm -M stm32-p103 -kernel mymain.bin  -nographic
 2 STM32_UART: UART1 clock is set to 0 Hz.
 3 STM32_UART: UART1 BRR set to 0.
 4 STM32_UART: UART1 Baud is set to 0 bits per sec.
 5 STM32_UART: UART2 clock is set to 0 Hz.
 6 STM32_UART: UART2 BRR set to 0.
 7 STM32_UART: UART2 Baud is set to 0 bits per sec.
 8 STM32_UART: UART3 clock is set to 0 Hz.
 9 STM32_UART: UART3 BRR set to 0.
10 STM32_UART: UART3 Baud is set to 0 bits per sec.
11 STM32_UART: UART4 clock is set to 0 Hz.
12 STM32_UART: UART4 BRR set to 0.
13 STM32_UART: UART4 Baud is set to 0 bits per sec.
14 STM32_UART: UART5 clock is set to 0 Hz.
15 STM32_UART: UART5 BRR set to 0.
16 STM32_UART: UART5 Baud is set to 0 bits per sec.
17 STM32_UART: UART5 clock is set to 0 Hz.
18 STM32_UART: UART5 BRR set to 0.
19 STM32_UART: UART5 Baud is set to 0 bits per sec.
20 STM32_UART: UART4 clock is set to 0 Hz.
21 STM32_UART: UART4 BRR set to 0.
22 STM32_UART: UART4 Baud is set to 0 bits per sec.
23 STM32_UART: UART3 clock is set to 0 Hz.
24 STM32_UART: UART3 BRR set to 0.
25 STM32_UART: UART3 Baud is set to 0 bits per sec.
26 STM32_UART: UART2 clock is set to 0 Hz.
27 STM32_UART: UART2 BRR set to 0.
28 STM32_UART: UART2 Baud is set to 0 bits per sec.
29 STM32_UART: UART1 clock is set to 0 Hz.
30 STM32_UART: UART1 BRR set to 0.
31 STM32_UART: UART1 Baud is set to 0 bits per sec.
32 LED Off
33 CLKTREE: HSI Output Change (SrcClk:None InFreq:8000000 OutFreq:8000000 Mul:1 Div:1 Enabled:1)
34 CLKTREE: HSI/2 Output Change (SrcClk:HSI InFreq:8000000 OutFreq:4000000 Mul:1 Div:2 Enabled:1)
35 CLKTREE: SYSCLK Output Change (SrcClk:HSI InFreq:8000000 OutFreq:8000000 Mul:1 Div:1 Enabled:1)
36 CLKTREE: HCLK Output Change (SrcClk:SYSCLK InFreq:8000000 OutFreq:8000000 Mul:1 Div:1 Enabled:1)
37 STM32_RCC: Cortex SYSTICK frequency set to 8000000 Hz (scale set to 125).
38 STM32_RCC: Cortex SYSTICK ext ref frequency set to 1000000 Hz (scale set to 1000).
39 CLKTREE: PCLK1 Output Change (SrcClk:HCLK InFreq:8000000 OutFreq:8000000 Mul:1 Div:1 Enabled:1)
40 CLKTREE: PCLK2 Output Change (SrcClk:HCLK InFreq:8000000 OutFreq:8000000 Mul:1 Div:1 Enabled:1)
41 CLKTREE: HSE Output Change (SrcClk:None InFreq:8000000 OutFreq:8000000 Mul:1 Div:1 Enabled:1)
42 CLKTREE: HSE/2 Output Change (SrcClk:HSE InFreq:8000000 OutFreq:4000000 Mul:1 Div:2 Enabled:1)
43 CLKTREE: PLLXTPRE Output Change (SrcClk:HSE InFreq:8000000 OutFreq:8000000 Mul:1 Div:1 Enabled:1)
44 CLKTREE: PCLK1 Output Change (SrcClk:HCLK InFreq:8000000 OutFreq:4000000 Mul:1 Div:2 Enabled:1)
45 CLKTREE: PLLCLK Output Change (SrcClk:PLLXTPRE InFreq:8000000 OutFreq:72000000 Mul:9 Div:1 Enabled:1)
46 CLKTREE: SYSCLK Output Change (SrcClk:PLLCLK InFreq:72000000 OutFreq:72000000 Mul:1 Div:1 Enabled:1)
47 CLKTREE: HCLK Output Change (SrcClk:SYSCLK InFreq:72000000 OutFreq:72000000 Mul:1 Div:1 Enabled:1)
48 STM32_RCC: Cortex SYSTICK frequency set to 72000000 Hz (scale set to 13).
49 STM32_RCC: Cortex SYSTICK ext ref frequency set to 9000000 Hz (scale set to 111).
50 CLKTREE: PCLK1 Output Change (SrcClk:HCLK InFreq:72000000 OutFreq:36000000 Mul:1 Div:2 Enabled:1)
51 CLKTREE: PCLK2 Output Change (SrcClk:HCLK InFreq:72000000 OutFreq:72000000 Mul:1 Div:1 Enabled:1)
52 CLKTREE: GPIOA Output Change (SrcClk:PCLK2 InFreq:72000000 OutFreq:72000000 Mul:1 Div:1 Enabled:1)
53 CLKTREE: AFIO Output Change (SrcClk:PCLK2 InFreq:72000000 OutFreq:72000000 Mul:1 Div:1 Enabled:1)
54 CLKTREE: UART2 Output Change (SrcClk:PCLK1 InFreq:36000000 OutFreq:36000000 Mul:1 Div:1 Enabled:1)
55 STM32_UART: UART2 clock is set to 36000000 Hz.
56 STM32_UART: UART2 BRR set to 0.
57 STM32_UART: UART2 Baud is set to 0 bits per sec.
58 STM32_UART: UART2 clock is set to 36000000 Hz.
59 STM32_UART: UART2 BRR set to 3750.
60 STM32_UART: UART2 Baud is set to 9600 bits per sec.
61 1.5
62 abc

正確印出 float 1.5, 別看我寫的很輕鬆, 期間可是克服不少問題才把這 1.5 辛苦秀出來。

我當然不會滿足模擬器上的測試, 以下是 stm32discovery 完整範例:
https://github.com/descent/stm32f4_prog/tree/master/float

我有 gcc 4.7, 4.8, 4.9 三個版本的 toolchain, 測試結果有所不同, stm32discovery 的版本我在 gcc 4.91 測試成功, 其他的都不正常。

debian 提供的 arm-none toolchain 有支援 mulit-lib, 什麼是 multi-lib, 很難解釋, 直接看:

debian64:~# dpkg -L libnewlib-arm-none-eabi|grep libm
/usr/lib/arm-none-eabi/newlib/libm.a
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/libm.a
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/fpu/libm.a
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/softfp/libm.a
/usr/lib/arm-none-eabi/newlib/fpu/libm.a
/usr/lib/arm-none-eabi/newlib/thumb/libm.a
/usr/lib/arm-none-eabi/newlib/thumb/armv7-ar/fpu/vfpv3-d16/be/libm.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/libm.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/fpu/libm.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/softfp/libm.a
/usr/lib/arm-none-eabi/newlib/armv6-m/libm.a
/usr/lib/arm-none-eabi/newlib/armv7-m/libm.a

debian64:~# dpkg -L libnewlib-arm-none-eabi|grep libc
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/fpu/libc.a
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/fpu/libc_nano.a
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/softfp/libc.a
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/softfp/libc_nano.a
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/libc.a
/usr/lib/arm-none-eabi/newlib/armv7-ar/thumb/libc_nano.a
/usr/lib/arm-none-eabi/newlib/fpu/libc.a
/usr/lib/arm-none-eabi/newlib/fpu/libc_nano.a
/usr/lib/arm-none-eabi/newlib/libc.a
/usr/lib/arm-none-eabi/newlib/thumb/armv7-ar/fpu/vfpv3-d16/be/libc.a
/usr/lib/arm-none-eabi/newlib/thumb/armv7-ar/fpu/vfpv3-d16/be/libc_nano.a
/usr/lib/arm-none-eabi/newlib/thumb/libc.a
/usr/lib/arm-none-eabi/newlib/thumb/libc_nano.a
/usr/lib/arm-none-eabi/newlib/libc_nano.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/fpu/libc.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/fpu/libc_nano.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/softfp/libc.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/softfp/libc_nano.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/libc.a
/usr/lib/arm-none-eabi/newlib/armv7e-m/libc_nano.a
/usr/lib/arm-none-eabi/newlib/armv6-m/libc.a
/usr/lib/arm-none-eabi/newlib/armv6-m/libc_nano.a
/usr/lib/arm-none-eabi/newlib/armv7-m/libc.a
/usr/lib/arm-none-eabi/newlib/armv7-m/libc_nano.a

同一套 toolchain 提供了不同版本的 libm.a, 這就是 multi-lib, 那要怎麼知道要 link 那一個, 施主, 程式你寫的, 自己要知道阿!

list 1.
1 descent@debian64:compiler$ arm-none-eabi-gcc -print-multi-lib
2  .;
3 thumb;@mthumb
4 fpu;@mfloat-abi=hard
5 armv6-m;@mthumb@march=armv6s-m
6 armv7-m;@mthumb@march=armv7-m
7 armv7e-m;@mthumb@march=armv7e-m
8 armv7-ar/thumb;@mthumb@march=armv7
9 armv7e-m/softfp;@mthumb@march=armv7e-m@mfloat-abi=softfp@mfpu=fpv4-sp-d16
10 armv7e-m/fpu;@mthumb@march=armv7e-m@mfloat-abi=hard@mfpu=fpv4-sp-d16
11 armv7-ar/thumb/softfp;@mthumb@march=armv7@mfloat-abi=softfp@mfpu=vfpv3-d16
12 armv7-ar/thumb/fpu;@mthumb@march=armv7@mfloat-abi=hard@mfpu=vfpv3-d16
13 thumb/armv7-ar/fpu/vfpv3-d16/be;@mthumb@march=armv7@mfloat-abi=hard@mfpu=vfpv3-d16@mbig-endian

-print-multi-lib 這個參數可以顯示出什麼參數會去用那個路徑的 libm。

descent@debian64:$ arm-none-eabi-gcc -print-file-name=libm.a
/usr/lib/gcc/arm-none-eabi/4.9.3/../../../arm-none-eabi/lib/libm.a

descent@debian64:$ arm-none-eabi-gcc -march=armv6s-m -mthumb -print-file-name=libm.a
/usr/lib/gcc/arm-none-eabi/4.9.3/../../../arm-none-eabi/lib/armv6-m/libm.a

list 1. L5 的 @mthumb@march=armv6s-m, 就是指 -mthumb -march=armv6s-m, 把 @ 換成 - 就可以了。

2014年11月2日 星期日

作業系統之前的程式 for stm32f4discovery (9) - CCM

我最近 (201410) 才從 datasheet 知道 ccm 這東西。stm32f4discovery 有 192k sram, 不過是這樣分佈的:

sram1: 112k
sram2: 16k
ccm (core coupled memory): 64k

sram1, sram2 從 0x2000 0000 開始的 128k
ccm 從 0x1000 0000 開始的 64K

192k 的位址並不連續, 而且 ccm 只能存資料, 無法存指令, 那麼該怎麼運用這塊記憶體呢? 我把他用在 stack 上, 有 64k 的 stack, 在這嵌入式平台上真是奢侈。

修改 linker script 加入一個 .ccm section, 以及在程式碼使用 __attribute__((section(".ccm"))), 將某個變數存放到 .ccm section。

stm32.ld
 1 /*
 2 MEMORY
 3 {
 4   FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
 5   SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
 6 }
 7 */
 8 
 9 /* Specify the memory areas */
10 /* form: STM32F4-Discovery_FW_V1.1.0/Project/Peripheral_Examples/IO_Toggle/TrueSTUDIO/IO_Toggle/stm32_flash.ld */
11 MEMORY
12 {
13   FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 1024K
14   CCM (rw)        : ORIGIN = 0x10000000, LENGTH = 64K /* 0x1000_0000 ~ 0x1000_FFFF. */
15   SRAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
16   MEMORY_B1 (rx)  : ORIGIN = 0x60000000, LENGTH = 0K
17 }
18 
19 
20 SECTIONS
21 {
22   .text :
23   {
24     KEEP(*(.isr_vector .isr_vector.*))
25     *(.text .text.*)
26     *(.rodata .rodata*)
27     _etext = .;
28   } > FLASH
29 
30   .ccm(NOLOAD):
31   {
32     . = ALIGN(8);
33     *(.ccm)
34     . = ALIGN(8);
35   } > CCM
36 
37   .data : AT (_etext)
38   {
39     _data = .;
40     *(.data .data.*)
41     _edata = .;
42   } > SRAM
43   .bss(NOLOAD) :
44   {
45     _bss = .;
46     *(.bss .bss.*)
47     *(COMMON)
48     . = ALIGN(4);
49     _ebss = .;
50   } > SRAM
51 
52   . = ALIGN(4);
53   _end = .;
54 }

ex:
#define STACK_SIZE 0xffff

__attribute__((section(".ccm")))
static u8 stack[STACK_SIZE];

stm32.h
  1 #ifndef STM32_H
  2 #define STM32_H
  3 
  4 #include "lib_mygpio_led.h"
  5 
  6 #define STACK_SIZE 0xffff
  7 extern unsigned long _etext;
  8 extern unsigned long _data;
  9 extern unsigned long _edata;
 10 extern unsigned long _bss;
 11 extern unsigned long _ebss;
 12 
 13 extern "C"
 14 {
 15   int mymain();
 16 }
 17 
 18 void ResetISR(void)
 19 {
 20   unsigned long *pulSrc, *pulDest;
 21 
 22   pulSrc = &_etext;
 23   for (pulDest = &_data; pulDest < &_edata;)
 24     *pulDest++ = *pulSrc++;
 25   for (pulDest = &_bss; pulDest < &_ebss;)
 26     *pulDest++ = 0;
 27 
 28   mymain();
 29 }

 94  __attribute__((section(".ccm")))
 95 static u8 stack[STACK_SIZE];
 98 __attribute__((section(".isr_vector")))
 99 pfnISR VectorTable[]=
100 {
101   (pfnISR)((unsigned long)stack + STACK_SIZE),
102   ResetISR, // 1
103   int_isr,
104   int_isr,
105   int_isr,
106   int_isr,
107   int_isr,
108   int_isr,
109   int_isr,
110   int_isr,
111   int_isr,
112   svc_isr,    // 11
113   int_isr,
114   int_isr,
115   pendsv_isr, // 14
116   systick_isr, // 15
117 
128 };
129 
130 #endif

以下是根據 stm32.ld 編譯出來的 bin 檔。

hexdump -C myur.bin
00000000 ff ff 00 10 41 00 00 08 35 01 00 08 35 01 00 08

前四個 byte 是 sp 的位址, 已經指到 ccm 區域。不過我用 gdb step 時, sp 的值是 0x1000fffc, 這是個疑問??是 alignment 的問題, 改成

#define STACK_SIZE 0xfff0, sp 就是 0x1000fff0。

完整範例:
https://github.com/descent/stm32f4_prog/tree/master/float

ref: http://wiki.csie.ncku.edu.tw/embedded/Flash