commonality(共通性)跟 variability(變化性)算是觀念,可以套用到軟體開發的不同階段。

在釐清客戶需求的階段,commonality 可以想成軟體「像」什麼,也就是系統需要做的事。variability 則是軟體「不像」什麼,即系統不用處理的事。透過釐清系統像什麼、不像什麼,來了解客戶心中想要的系統究竟要做什麼事。

到了 design 及 implement 階段,分析 class 間的 commonality,可以將共同的部分抽到 base class。

很久很久以前(大概是還在用紙帶打洞的時代),會在 code 裡直接寫 variable 跟 function 所在的 address(我猜一開始說不定只有「要在哪裡取得資料」跟「要跳到哪裡繼續執行」的概念)。增加指令、修改程式後,會有很多 variable 及 function 的 address 被改變,像是中間多塞個指令就會讓後面東西 address 全改了,所以程式中所有 address 都需要重新調整(relocate)。

這種 relocate 工作太令人崩潰,於是有人想出 symbol 的概念──以符號代表某個 variable 或 function(其實就是取名字)。code 裡改用 symbol,不再直接寫 address。等程式要執行的時候再把 symbol 換成真正的 address。這就是最開始 linking 主要做的事。

隨著時代演進(?),程式規模越來越大,人們開始在一個程式中分許多 module 並且分別 compile。有多個 module 後 linking 就要處理跨 module 的 variable 及 function 引用,也就是將在其他 module 中的 variable 跟 function 的 address 填入 reference 到它們的地方。例如在 A module 裡 call B module 的 function foo(),就要在 A module 中填入 foo() 真正的 address。

如此一來,compile 階段可以不用知道 symbol 的 address,而且 module 也可以獨立 compile,等到 link 階段再由 linker 處理 symbol 及 address 的轉換。linker 就像是將多個 compile 好的 module 黏起來。

linking 主要過程:

  • Address and storage allocation
  • Symbol resolution
  • Relocation

object file 是 source code compile 後但尚未 link 的 binary 中間檔,通常跟可執行檔使用相同格式。object file 包含 compile 後的 machine code、data、symbol table、debug 資訊及字串等等。

object file 以不同 section 儲存不同資訊。

Object File Overview

  • File Header 描述檔案屬性、section table
  • .text.code)section 裡放 code
  • .data section 放已經 initialize 的 global varible 及 static local variable
  • .bss section 記錄沒有 initialize 的 variable,先記著「有這個 variable」跟它的 size。但是因為 variable 還沒有 value,所以在 object file 裡不需要浪費空間去記錄 value。

Ref

Executable Format 主要有:

  • Windows 下的 PE(Portable Executable)
  • Linux 下的 ELF(Executable Linkable Format)

其他還有 Intel/Microsoft 的 OMF(Object Module Format)、Unix 的 a.out 以及 MS-DOS COM 格式等等。

除了可執行檔外,Dynamic Linking Library 跟 Static Linking Library 都是以 Executable Format 儲存。

ELF 標準中將使用 ELF 格式的檔案分成:

ELF file type Example
Relocatable File Linux 的 .o、Windows 的 .obj
Executable File /bin/bash、Windows 的 .exe
Shared Object File Linux 的 .so、Windows 的 .dll
Core Dump File Linux 的 core dump

ELF file structure

ELF 檔是由 header、一堆 section 及一堆 table 組成的,各 table 也是 section。

ELF structure

ELF Header

描述整個 ELF 檔的屬性,可用 readelf -h xxx 查看。

struct 定義在 /usr/inclue/elf.hElf32_Ehdr or Elf64_Ehdr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;

e_ident 是 magic number 標示這是個 ELF 檔,前 4 個 byte 依序是 0x7f、0x45、0x4c、0x446,接著 3 個 byte 是 “ELF” 三個字母的 ASCII code。所有 ELF 檔前幾個 byte 都是這個內容。除此之外,幾乎所有可執行檔最開始幾個 byte 都是 magic number,供 OS 識別是哪種可執行檔。

e_phoff 表示程式執行時的入口位置,executable file 會填入 address,relocatable file 因為還會進行 relocate 所以值是 0。

從 section header table file offset e_shoff 可以知道 section table 所在位置,由 e_shentsizee_shnum 可以知道 section header table 的 element size 以及總共有多少 element。從 ELF header 可以找到 section header table,由於其他 table 也都是 section,可以再從 section header table 取得所有其他 section 及 table 的資訊。

Section Header Table

描述各 section 的屬性,可用 readelf -S xxx 查看。

一個以 struct Elf32_Shdr(又稱 section descriptor)為 element 的 array,array 的第一個 element 是 NULL,struct 定義在 /usr/include/elf.h。因為 section header table 是個 array,所以 ELF 檔有些地方會以 section 在 section table 中的 index 來 access 或表示該 section。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;

如果一個 section 存在 ELF 檔中,由 sh_offsetsh_size 可以知道其所在位置及大小。section 屬性主要由 section type sh_type 及 section flags sh_flags 決定。

String Table

.strtab 以及 .shstrtab section。

將 ELF 檔裡所用的字串,如 variable name、function name、section name 等存在一個 array 中,以 '\0' 隔開,並以字串在 array 中的 offset 表示該字串。

Symbol Table

.symtab section,可用 readelf -s xxx 查看。

記錄 object file 所用到的 symbol。每個 symbol 有其對應的 symbol value,variable 及 function 的 symbol value 是它們的 address。

symbol table 是以 struct Elf32_Sym 為 element 的 array,Elf32_Sym 一樣定義在 /usr/include/elf.h

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

對 linking 較重要的兩種 symbol:global symbol 及 external symbol。global symbol 是定義在此 object file 中並且會讓其他 object file 使用到的 symbol,external symbol 則是此 object file reference 到的其他 object file 中的 symbol。簡單講就是給別人用跟用別人的 symbol,有點繞舌。

Relocation tables

存 relocation 的資訊,.rel.text section。每個需要 relocate 的 section 都會有一個 relocation table。

relocation 可參考 Static Link

Linking View and Execuion View

上述以 section 來劃分 ELF 內容的角度是 Linking View

ELF 在 mapping 到 virtual address space 時是以 page 為單位,如果 section 的大小不是 page 大小的整數倍又以 section 為單位進行 mapping,會浪費許多 memory。load ELF 時 OS 只在乎 section 的屬性如可讀、可寫、可執行,OS 不在乎 section 的內容,為了節省記憶體空間延伸出「將屬性相同的 section 合併成一個 segment,再對應到 virtual address space」的方式。linker 在 link object file 時會盡量將相同屬性的 section 放在一起。

segment 是以 load 的角度劃分 ELF,也就是 Execution View

Ref

定義演算法家族,將個別演算法封裝起來,讓它們可以互相替換。此 pattern 讓演算法的變動不會影響到使用演算法的部分。

Strategy pattern 是一種定義一系列演算法的方法,所有演算法完成的都是相同的工作,只是實作不同,它可以以相同的方式 call 所有演算法,減少了各種演算法 class 與使用演算法的 class 間的耦合。

例子:最短路徑搜尋

Strategy Pattern 最短路徑搜尋 class diagram

將 shortest path algorithm 封裝起來,讓 RouteFinder 使用。RouteFinder 使用 ShortestPathStrategy 找 shortest path(RouteFinder 將找路徑的工作 delegate 給 ShortestPathStrategy),ShortestPathStrategy 的 instance 則依據使用哪種 algorithm 決定。

有新的 algorithm 或 strategy 時只要 implement ShortestPathStrategy 並換掉 RouteFinder 使用的 instance 即可。

使用 RouteFinder 的 client code 跟演算法 strategy 的 class 就是分開、不互相耦合的。

Ref

機械式開關切換時訊號會有 bounce(彈跳)現象。

switch bounce

上圖為理想狀況,下圖為實際上有 bounce 的狀況。bounce 持續時間約為 10~20 ms。

程式在 bounce 期間會讀取到 bounce 的訊號造成判斷錯誤。例如按一下按鈕開關我們期望程式只讀到一次 on 的訊號,bounce 會造成程式讀到很多次 on 的訊號。

解決方式軟體及硬體皆有,硬體的我看不懂所以這裡只說軟體。(欸)

軟體解決方式:不處理 bounce 時間內的訊號。「不處理」的實際做法可以自己硬寫也可以在 add_event_detect() 裡設定。

圖片來源:http://www.bbc.co.uk/schools/gcsebitesize/design/electronics/switchesrev2.shtml

接 button、switch 時會用到。(看到 button 這字想到的是 GUI 上的…)

電路中希望維持一個電位基準值,好判斷某個電壓值是 0 還是 1。pull-up 跟 pull-down 電阻就是用來維持基準電位的,如果電路沒有接個東西,程式會讀到雜訊(亂數值)。

Pull-up resistor

switch 斷開時上半部的電路是通的,logic gate 會讀到較高的電位。switch 接上後,logic gate 的電位會變低(從電流來看是下半部電路會接通,以至於電位會改變)。如果認定 switch 接上是邏輯的 1 則 logic gate 是低電位時表示邏輯的 1,高電位表示邏輯的 0。

Pull-down resistor

pull-down 電阻是反過來,switch 斷開時 logic gate 會讀到低電位,接上後是高電位。所以高電位表示邏輯 1,低電位表示邏輯 0。

接 Raspberry Pi 時,上面說的 logic gate 就是 Raspberry Pi 的 GPIO pin。

murmur:被高低電位搞得頭有點暈,不太確定那個電流的理解對不對?沒搞得很懂 logic gate 在乎的是電位還是電位差……我的電路學只有高中程度啊……Orz

圖片來源及 Ref:

最近的感想。

念書的時候常覺得書上的理論跟實際上的使用或實作有一段距離,我說不上來那是落差還是什麼,但就是有點搭得起來又哪裡怪怪的。後來工作後我有點疑問──在學校念的那些理論,真的有用嗎?如果業界是以實務為主,為什麼我們需要學那些理論?從這個問題就看得出來我當學生的時候很少好好理解過為啥自己要念那些書…

我最近得到的答案是──理論會提供我們背景知識與架構,可以讓我們能較快速的理解系統跟軟體的運作方式,而不至於迷失在茫茫程式碼海中,要從許多的片段中慢慢拼湊才能知道整個系統的樣貌。當然,理論提供的是偏向概念上的了解,它不會有太細部的東西,實務上的細節是會隨著情境不同而調整的。

軟體的結構也一樣。最近工作上,我發現因為對整個軟體的架構比較熟悉了,所以能比較快猜測、反應出可能是哪邊有問題,不像之前一遇到問題就得重新 trace,也似乎比較看得出來改動哪個部分會影響到那些東西。開始稍微能體會軟體有其架構的重要性,當它有個結構、有些規則可循,programmer 比較能知道改動的影響範圍,出問題時也能比較快找到。我們可以不用去記憶一些特例跟「這邊改了那邊還有那那邊也需要改」的事情,這也會減少出錯的可能。對我這種不喜歡記那麼多細節還會搞混的人來說這真是太棒了!

最近在看《深入淺出設計模式》,我想 design pattern 也是一種軟體上的結構,如果知道某個部分是使用某個 pattern,就能馬上了解其大致的運作。不過我不了解 pattern 的時候,看那些 code 只有一個感想──幹嘛把事情搞得那麼複雜?

可惜的是,我現在仍然不覺得「軟體有其架構帶來的效益」這件事能有精確量化的指標。因為沒有對照組,沒有一套沒有架構的同樣軟體去比較有架構與沒有架構的差別,可能只能用間接的方式去量度關於軟體有架構的益處。

封裝會改變的東西,減少改動一個部分會影響到其他部分的狀況。

將 class 中容易改變的部分封裝到另一個 class 有助於保護原本 class 有不必要的改變。

程式改變可能改動到相關功能,如果一個 class 中常修改的地方並非該 class 負責的事情,修改它是不必要的,而且可能引入其他功能受到影響的風險。如果能將常修改的地方封裝,就可以保護原本 class 的其他功能不受影響。

「封裝」不單指將東西包成 class,把一堆 property 放到 map 之類的 container 也是封裝。

A cohensive class does one thing really well and does not try to do or be something else.

來來來翻譯一下:一個 class 只做一件事,不會插手做別人的事。

但是,這「一件事」的大小跟規模是由設計的人定義的,可以很大也可以很小,定義清楚即可。

class 內聚力越高,class 間的耦合度(改動一個 class 就要改動其他 code 的程度)越低,越容易 reuse 及擴展。

讓一個 class 只做一件事,只有那件事情需要修改時才會讓這個 class 改變。這樣每次修改的影響範圍可以縮小,可能產生 bug 的範圍也就縮小啦。另一個好處是 debug 的時候,programmer 可以較迅速的知道可能出問題的範圍,降低時間成本。

檢查內聚力的方式:做一項改變時是否牽動到許多 class?是的話表示內聚力低、耦合度高,程式不易修改跟擴充。