2014年5月18日 星期日

C++ inline namespace

最近在研究 libc++ 的原始碼,意外的發現 inline namespace 這個有趣的功能。一言以蔽之,inline namespace 讓我們可以在 C++ 程式碼層級做 symbol versioning。

設想一個情境:你今天要設計一個函式庫。你可以預期這個函式庫的生命週期會很長,而且你未必能讓所有的使用者重新編譯他們的程式。這意謂著:你對 Object Layout 的任何改動都有可能破壞向下相容。這時候我們就會想要提供不同版本的函式。但是實務上該怎麼做呢?

舉例來說,我們要實作 std::string 類別。最早我們寫的版本是一個簡單的實作:

namespace std {
  class string {
  private:
    size_t size;
    size_t capacity;
    char *data;
  public:
    string(const char *s);
    const char *c_str() const {
      return data;
    }
  };

  string::string(const char *s)
      : size(std::strlen(s)), capacity(0), data(nullptr) {
    data = new char[size + 1];
    capacity = size + 1;
    memcpy(data, s, size + 1);
  }
}


可是隨著時間的演進,我們想採用 Small String Optimization (SSO) 以取得更高的效能與空間利用率。這時候我們必需把 std::string 改寫成:

namespace std {
  class string {
  private:
    size_t size;

    union {
      struct {
        size_t capacity;
        char *data;
      } normal;

      struct {
        char data[sizeof(size_t) + sizeof(char *)];
      } sso;
    };

  public:
    string(const char *s);

    const char *c_str() const {
      if (size < sizeof(sso.data)) {
        return sso.data;
      } else {
        return normal.data;
      }
    }
  };

  string::string(const char *s): size(std::strlen(s)) {
    if (size < sizeof(sso.data)) {
      memcpy(sso.data, s, size + 1);
    } else {
      normal.data = new char[size + 1];
      normal.capacity = size + 1;
      memcpy(normal.data, s, size + 1);
    }
  }
}


然而這個改動有可能會對舊的 Binary 造成困擾。如果 c_str() 有被 inline 但 string(const char *) 建構子沒有被 inline,則執行期會產生「字串內容」被當成 data_ 指標的嚴重問題。

我們該怎麼避免這個問題呢?我們可以用 Namespace 適當分割不同的實作:

namespace std {
  namespace v1 {
    class string { ... };
  }
  namespace v2 {
    class string { ... };
  }
}


可是這樣我們必須用 std::v1::string 或 std::v2::string 來指稱我們的 string 類別。這當然不滿足我們的期望。畢境所有的使用者還是使用 std::string 宣告一個字串。

這時就輪到 inline namespace 出場了!嚴格來說,inline namespace 是編譯器提供的 syntax sugar。他讓我們可以省略那個 namespace 的 scope 運算子。舉例來說,如果我要以 std::v2::string 的實作作為預設的版本,我可以把上面的例子改成:

namespace std {
  namespace v1 {
    class string { ... };
  }
  inline namespace v2 {
    class string { ... };
  }
}


這樣一來,我寫 std::string 其實就會是 std::v2::string。

當然,你也可以善用 Preprocessor 讓不同的程式可以依據特定條件選擇特定實作。例如:libfoo 是舊的函式庫,所以如果 libfoo 引入這個標頭檔,我們就把 v1 宣告為 inline namespace;libbar 是新的函式庫,所以如果 libbar 引入這個標頭檔,我們就把 v2 宣告為 inline namespace。

namespace std {
#if defined(LIBFOO)
  inline
#endif
  namespace v1 {
    class string { ... };
  }


#if !defined(LIBFOO)
  inline
#endif
  namespace v2 {
    class string { ... };
  }
}


附帶一提,inline namespace 只是 syntax sugar 對我們有一個實際上的好處:從 Linker 或 Loader 的角度來看,這些都是不同的函式,所以 std::v1::string 和 std::v2::string 的實作可以並存。不同來源的 Binary 可以各自取用他們所需的實作。

2014年5月3日 星期六

使用 git subtree 切分版本庫

有候我們希望把一個大的 Git Repository 切分成多個 Repositories。這時我們就可以用 git subtree 這個指令。以下我以 Android NDK 為例子:

$ git clone https://android.googlesource.com/platform/ndk
$ cd ndk

分割 sources/cxx-stl/llvm-libc++abi/libcxxabi 下面的 commits:

$ git subtree split -P sources/cxx-stl/llvm-libc++abi/libcxxabi -b libcxxabi

之後只要另外在不同的資料夾執行:

$ cd ..
$ git init libcxxabi
$ cd libcxxabi
$ git pull ../ndk libcxxabi:master

就可以有一個完全獨立的版本庫了!

參考資料

2014年4月21日 星期一

Ubuntu 14.04 與 ASUS UX32VD 的小問題

因為 Ubuntu 12.04 不能正常驅動 ASUS UX32VD 的顯示卡,所以一直以來我都是用 Ubuntu 13.04 的 Kernel 搭配 12.04 的應用程式。這次釋出的 Ubuntu LTS 14.04 初步測試沒有問題就升級了。不過目前有遇一些小問題,暫時用以下方法解決:

內建的喇叭沒有辦法發出聲音


不知道為什麼我不在 audio 群組之內,所以我只能從外接孔聽到聲音。把我自己加到 audio 群組即可。

$ sudo adduser $USER audio

IBUS 變得不太討喜


新版的 IBUS 除了不能用 Ctrl+Space 切換中英輸入之外(需改用 Shift),會跟著插入點亂跑的輸入法狀態視窗真的很礙眼。因此目前暫時改用 GCIN:

$ sudo apt-get install gcin

之後再去「系統設定值 / 語言支援 / 鍵盤輸入法系統」選擇 GCIN。

不過 GCIN 似乎也有一些問題,無法從狀態列看到 GCIN 的圖示。或許是 Unity 或 AppIndicator 的問題,但是初步測試,用 dconf-editor 修改「com / canonical / unity-gtk-module / whitelist」也沒有用。

SVN 格式更新


以前的 Subversion 版本庫要用以下指令轉換檔案格式:

$ svn upgrade

預設似乎沒有辦法用 apt-get 安裝 i386 的軟體


$ sudo dpkg --add-architecture i386
$ sudo dpkg --print-foreign-architectures
i386

Ubuntu 14.04 無法正常讀取部分使用 PTP 協議的相機


部分過去在 12.04 可以正常使用的數位相機,在 14.04 就沒有辦法正常的被偵測到。舉例來說,我使用的 Nikon Coolpix S6200 就中標了。從錯誤回報來看,應該是 gvfs 有點問題。

除了直接改用讀卡機存取照片外,另一個可行的方法是使用 gphoto2 這個 command line 工具。

$ sudo apt-get install gphoto2
$ mkdir ~/Camera
$ cd ~/Camera
$ gphoto2 -P

小結


目前就只遇到這些問題,其他以後想到再補好了。

2013年9月6日 星期五

編譯 Android AOSP

先前曾經寫過一篇怎麼下載 Android AOSP 原始碼的文章。不過還沒說怎麼編譯 Android Open Source Project。雖然官方網頁有很詳細的說明,但我自己有遇到一些無法執行的步驟,而且有很多可選的步驟不太好讀。所以我就花一點時間把我自己的步驟寫下來。希望可以拋磚引玉。

準備編譯環境


安裝套件


根據官方的說明文件,最適合編譯 Android 的開發環境為 Ubuntu 12.04 (x86_64)。以下文章我們假設大家的環境都是 Ubuntu 12.04。首先在開始之前,我們必須要安裝一些必要的套件:

$ sudo apt-get install git gnupg flex bison gperf build-essential \
    zip curl libc6-dev libncurses5-dev:i386 x11proto-core-dev \
    libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-glx:i386 \
    libgl1-mesa-dev g++-multilib gcc-multilib mingw32 tofrodos \
    python-markdown libxml2-utils xsltproc zlib1g-dev:i386


另外為了可以讓 Build System 找得到  Mesa3d 函式庫,我們必須為他建立一個 Soft Link:

$ sudo ln -s /usr/lib/i386-linux-gnu/mesa/libGL.so.1 /usr/lib/i386-linux-gnu/libGL.so

如果你在 apt-get install 時,遇到類似下面的錯誤(沒有遇到可以直接跳到 Java 的段落):

E: Unable to locate package mingw32
E: Package 'python-markdown' has no installation candidate

這是因為你的 APT 沒有開啟 Universe 來源(也就是第三方維護的自由軟體)。我們要先把他加進 Ubuntu 軟體來源,然後再更新套件資訊。

$ sudo vim /etc/apt/sources.list
# 找到 "deb http://archive.ubuntu.com/ubuntu precise main" 在後面加上 universe

$ sudo apt-get update

等到 apt-get update 執行完,再從頭開始執行應該就不會有問題了。

安裝 OpenJDK 7 JDK


如果要編譯最新的 Android,你會需要 Java 7。你可以用以下指令安裝 OpenJDK 7:

$ sudo apt-get install openjdk-7-jdk

之後如果我們要編譯 Android 之前,我們必須記得:

$ export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64
$ export PATH=$JAVA_HOME/bin:$PATH


安裝 Java SE 6 JDK


如果你是要編譯較舊的 Android,你會需要 Java SE 6 JDK,如果你是要編譯最新的 Android,可以跳過這一節。我們必需到 Oracle 網站取得 Java SE Development Kit 6u45 (Java SE 6 JDK)。因為我們的環境是 Ubuntu,所以我們要下載的是  jdk-6u45-linux-x64.bin。下載之後,就依下述指令安裝:

$ chmod +x ./jdk-6u45-linux-x64.bin
$ ./jdk-6u45-linux-x64.bin
$ mv jdk1.6.0_45 /opt

之後如果我們要編譯 Android 之前,我們必須記得:

$ export JAVA_HOME=/opt/jdk1.6.0_45
$ export PATH=$JAVA_HOME/bin:$PATH


取得 repo 管理工具


因為 Android 是一個很大的專案,由數百個 Git 版本庫所構成。為了管理這些版本庫,Google 開發了 repo 這個 Git 管理工具。透過 repo 我們可以一次下載整個 Android 原始碼樹。我們必須先取得 repo:

$ mkdir ~/bin
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod +x ~/bin/repo
$ export PATH=~/bin:$PATH

取得原始碼與 Prebuilt Binaries


下載 Android 原始碼


和先前那篇一樣,我們現在可以下載 AOSP 的原始碼了!我們先建立一個資料夾,再使用 repo init 取得 Android Source Tree 的設定檔。最後使用 repo sync 把所有 Git 版本庫抓到本地端。

$ mkdir ~/android-src
$ cd ~/android-src
$ repo init -u https://android.googlesource.com/platform/manifest
$ repo sync -j2

下載 Prebuilt Binaries


如果要刷手機,我們還需要一些來自廠商的驅動程式或 Prebuilt Binaries。我們可以到 Binaries for Nexus Devices 找到 Nexus 系列所需的驅動程式。下載之後(以 broadcom-maguro-jdq39-8edbeae8.tgz 為例),我們先把壓縮檔放在 ~/android-src,然後:

$ cd ~/android-src
$ tar zxvf broadcom-maguro-jdq39-8edbeae8.tgz
$ ./extract-broadcom-maguro.sh
# 按下 Enter,再按下 q,輸入 "I ACCEPT" 之後按下 Enter 即可。

編譯 Android 原始碼


取得所有程式碼之後我們就可以開始編譯整個 Android。首先我們必需先設定好環境變數:

$ cd ~/android-src
$ export JAVA_HOME=/opt/jdk1.6.0_45
$ export PATH=$JAVA_HOME/bin:$PATH
$ source build/envsetup.sh

選取你想要編譯的目標。一般來說 full-eng 就可以滿足所需。如果你要刷手機,可能要從 Building for Devices 頁面找到合適的目標。

$ lunch full-eng

接下來我們就可以開始編譯了!

$ make -j16
(...中略...)
Install system fs image: out/target/product/generic/system.img
out/target/product/generic/system.img+ maxsize=588791808 blocksize=2112 total=576716800 reserve=5947392

經過漫長的等待,如果你可以在最後看到類似上面的訊息,那整個 Android 就成功完成編譯工作,我們可以打開模擬器或刷手機了。

開啟模擬器


開啟模擬器的方法非常容易,只要輸入以下指令:

$ ./out/host/linux-x86/bin/emulator

  • 根據我的測試,有些目標例如 full_maguro-eng 沒辦法使用模擬器。如果要使用模擬器要改用 lunch full-eng。
  • 如果你之後要使用模擬器,但是看到:

    emulator: ERROR: You did not specify a virtual device name, and the system directory could not be found.

    那是因為你忘記 lunch full-eng 了。我們必需 lunch full-eng 模擬器才能找到對應的 System Image。

刷手機


在刷手機之前,最好先上網尋找有沒有成功的案例。因為刷機一定會違反保固條款失去保固。其次,如果指令錯誤更有可能讓你的手機變成磚塊。一般來說,Google 推出的 Nexus 系列都沒有什麼大問題。但是因為不同的手機有不同的狀況,本文僅供參考,不為後果負責

首先我們先看 adb 能不能抓到你的手機:

$ adb devices

  • 如果看不到你的裝置,有可能是 udev 設定檔沒有設好。可以參考 Configuring USB Access,建立 /etc/udev/rules.d/51-android.rules。
  • 如果你有看到裝置,但是在 offline 的狀態。這有可能是因為你沒有開啟手機的開發者選項。你可以拿起手機,進入「Setting/About Phone/Build Number」,連續點 7 下。之後在「Developer Options」頁面勾選「USB Debugging」。

接下來我們要開機進入刷機模式:

$ adb reboot bootloader

這時我們可以看到手機進入 Bootloader,我們可以用 fastboot devices 尋找手機:

$ fastboot devices

如果你還沒有 Unlock 手機,這時我們就可以 Unlock 他。注意:這一步會讓保固失效!

$ fastboot oem unlock

最後,我們來刷手機。刷手機的過程絕對不能拔手機線。

$ fastboot flashall

除了把新的映象檔刷進手機,我們還要清空所有 User Data 與 Cache。否則 Android 有一定的機會一直停留在開機畫面。

$ fastboot erase userdata
$ fastboot erase cache

最後我們就可以重新開機!

$ fastboot reboot

我們可以用 adb logcat 觀察開機過程。有時候會遇到一直開不了機的情況。這時可以先試著清空 User Data 與 Cache。如果還是有問題,有時候是 AOSP 有問題,可以先刷回官方的映象檔。

結語


本文簡單的介紹怎麼編譯 Android Open Source Project 與刷機的基本流程。 希望大家看完之後有能力自己 Hack Android。Happy hacking!

2013年7月31日 星期三

dlopen() 與 GCC -rdynamic 選項

之前讀某個 JIT Compiler 的程式碼。只要正確輸入函式原型,這個 JIT Compiler 可以讓使用者呼叫外部的 C 函式。

要實作類似的功能並不困難,我們只要寫一個「名稱—位址」的對照表,JIT Compiler 就能輕易地實作這個功能。舉例來說,如果我希望使用者可以使用 foo 與 bar 二個函式,我們需要定義一個簡單的函式表:

struct { const char *, void * } func_table[] = {
  { "foo", (void *)&foo },
  { "bar", (void *)&bar },
  { NULL, NULL },
};


當使用者輸入 foo(),我們就可以從函式表取得 foo 的位址,進而呼叫 foo 函式。不過我卻沒有辦法在 JIT Compiler 的程式碼找到類似的東西。這激起了我的興趣。

其實 JIT Compiler 的執行檔確實是有這樣的函式表。如果仔細看該 JIT Compiler 的 Makefile,我們就可以發現他在編譯 JIT Compiler 的時候,額外加上 -rdynamic 選項。根據 GCC 使用手冊,-rdynamic 的用途為:
Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of dlopen or to allow obtaining backtraces from within a program.
簡單的說,這個選項會把有的 non-static 函式視為要 export 的 symbol,讓 Linker 把他們都寫入 Dynamic Symbol Table。之後,我們就可以使用 dlsym() 找到這個 symbol。以下是一個簡單的範例:

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>


void foo() {
  printf("called foo()\n");
}

void bar() {
  printf("called bar()\n");
}

int main(int argc, char **argv) {
  if (argc < 2) {
    fprintf(stderr, "USAGE: %s [func]\n", argv[0]);
    return EXIT_FAILURE;
  }

  /* Load the function/variable symbol table from executable. */
  void *handle = dlopen(NULL, RTLD_LAZY);
  if (!handle) {
    fprintf(stderr, "ERROR: Failed to load executable\n");
    return EXIT_FAILURE;
  }

  /* Find the function */
  void *func = dlsym(handle, argv[1]);
  if (!func) {
    fprintf(stderr, "ERROR: Function not found: %s\n", argv[1]);
  } else {
    ((void (*)())func)();
  }

  dlclose(handle);

  return EXIT_SUCCESS;
}


我們可以分別使用以下二個指令編譯這個範例程式:

$ gcc example.c -ldl
$ gcc example.c -ldl -rdynamic

然後使用 objdump -T 指令觀看執行檔的 Dynamic Symbol Table。

$ objdump -T a.out

以下是簡化過的輸出:

a.out:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
... 略 ...
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.2.5 fwrite
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.2.5 dlsym
0000000000601048 g    D  *ABS*    0000000000000000  Base        _edata
0000000000400914 g    DF .text    0000000000000010  Base        bar
0000000000601038 g    D  .data    0000000000000000  Base        __data_start
0000000000400904 g    DF .text    0000000000000010  Base        foo
... 略 ...

0000000000400924 g    DF .text    00000000000000f6  Base        main
0000000000400788 g    DF .init    0000000000000000  Base        _init
0000000000601048 g    DO .bss    0000000000000008  GLIBC_2.2.5 stderr
... 略 ...


其中藍色的字就是加上 -rdynamic 選項多出來的資料。執行以下指令,我們就可以注意到範例程式可以呼叫對應的函式:

$ ./a.out foo
called foo()
$ ./a.out bar
called bar()

我們要怎麼取得 Dynamic Symbol Table 裡面的資料呢?這就必須借助 dlopen()dlsym()。根據 man dlopen 的資料,dlopen() 必須要傳入二個參數,第一個參數是要載入的動態函式庫,而第二個參數是函式庫的載入方式。如果我們以 NULL 作為第一個參數,則代表我們要載入執行檔本身的 Dynamic Symbol Table。接著我們就可以用 dlsym() 尋找我們要執行的函式了。

後注:雖然我覺得這個方法很巧妙也很有趣,不過我覺得這個方法不太安全。因為所有的函式都有可能暴露給 JIT Compiler 使用者。我覺得還是手動建函式表會比較好。這樣我們才可以控制要開放的函式與介面。

2013年2月20日 星期三

Ubuntu 12.04 GVim Workaround for Hanging

Create a file named "gvim" in your $PATH, add following contents, and "chmod +x gvim"

#!/usr/bin/python

import subprocess
import sys

def main():
    dev_null = open('/dev/null', 'w+')
    arg = ['/usr/bin/gvim', '-f'] + sys.argv[1:]
    subprocess.Popen(arg, stdout=dev_null, stderr=dev_null)

if __name__ == '__main__':
    main()

2013年2月19日 星期二

Latex Beamer Itemize

一直以來都覺得 beamer itemize 的項目間距太擁擠了。如果要把他調高一點,可以用:

\begin{itemize}
    \addtolength{\itemsep}{0.25\baselineskip}}
    \item{Item 1}
    \item{Item 2}
\end{itemize}

當然如果一個一個加很麻煩,也可以定義一個新的 environment:

\newenvironment{xitemize}
{\begin{itemize}\addtolength{\itemsep}{0.25\baselineskip}}
{\end{itemize}}

這樣要用間距比較大的 itemize,就改成

\begin{xitemize}
    \item{Item 1}
    \item{Item 2}
\end{xitemize}

2013年1月20日 星期日

動手寫 Linux Driver

先前為了一個期末專題花了一點時間研究怎麼在 Linux 作業系統上寫一個 PCI Driver。寫過 Linux 驅動程式之後,覺得 Linux 的架構真的很漂亮!為了怕以後忘記怎麼寫,所以就把他寫下來記錄成一篇文章。

建構編譯環境


先我們必須要準備開發 Linux 驅動程式所需的環境,在 Debian 上可以用以下的指令達到這個目的:

$ sudo apt-get install build-essential linux-headers-$(uname -r)

其中 build-essential 會安裝 gcc, make 等軟體開發必要的工具,而 linux-headers 會安裝開發 Linux 驅動程式必要的 SDK。因為 linux-headers 會隨核心的版本而有所不同,所以我們要使用 $(uname -r) 取得目前核心的版本。

簡單的驅動程式


所有的 Linux 驅動程式至少要包含一個 MODULE_LICENSE 用以宣告驅動程式的授權,另外還需要一個 init 與一個 exit 函式,分別處理驅動程式的起始與終止。以下就是一個什麼都沒有的空殼:

/* example.c */
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int example_init(void) {
    printk("<1>EXAMPLE: init\n");
    return 0;
}

static void example_exit(void) {
    printk("<1>EXAMPLE: exit\n");
}

module_init(example_init);
module_exit(example_exit);

我們可以注意到裡面有一個 printk,他就相當於驅動程式設計當中的 printf。我們如果需要印任何除錯資訊,可以呼叫 printk,然後使用 sudo dmesg 觀看結果。編譯這個檔案之前,我們要先幫他寫 Makefile:

obj-m := example.o

ifeq ($(KERNELDIR),)
KERNELDIR=/lib/modules/$(shell uname -r)/build
endif

all:
    make -C $(KERNELDIR) M=$(PWD) modules

clean:
    make -C $(KERNELDIR) M=$(PWD) clean

在這個 Makefile 裡面,我們會使用 obj-m 這個變數指定我們要編譯的模組,然後再呼叫 make 讓他載入 SDK 的 Makefile。我們先前安裝的 SDK 就會放在 /lib/modules/$(shell uname -r)/build 裡面。

接下來我們就可以用 make 編譯我們的模組,並使用以下指令載入、卸除模組:

$ sudo insmod ./example.ko
$ sudo rmmod example

如果要看我們的模組有沒有輸出任何訊息,可以使用:

$ sudo dmesg | tail

註冊為 Character Device


在 Unix 的設計哲學當中,所有的東西都是檔案,硬體也不例外。我們寫驅動程式的時候要提供一個檔案操作的介面給 Userspace 的程式。為了達到這個目的,我們必須再引入一個標頭檔:

#include <linux/fs.h>

然後定義若干檔案操作與 file_operations 這個資料結構:

static int example_open(struct inode *inode, struct file *filp) {
    printk("<1>EXAMPLE: open\n");
    return 0;
}

static int example_close(struct inode *inode, struct file *filp) {
    printk("<1>EXAMPLE: close\n");
    return 0;
}

static ssize_t example_read(struct file *filp, char *buf, size_t size, loff_t *f_pos) {
    printk("<1>EXAMPLE: read  (size=%zu)\n", size);
    return 0;
}

static ssize_t example_write(struct file *filp, const char *buf, size_t size, loff_t *f_pos) {
    printk("<1>EXAMPLE: write  (size=%zu)\n", size);
    return size;
}

static struct file_operations example_fops = {
    .open = example_open,
    .release = example_close,
    .read = example_read,
    .write = example_write,
};

然後在 example_init() 當中用 register_chrdev 把這個驅動程式註冊為一個 Character Device。

#define EXAMPLE_MAJOR 60
#define EXAMPLE_NAME "example"

static int example_init(void) {
    int result;
    printk("<1>EXAMPLE: init\n");

    /* Register character device */
    result = register_chrdev(EXAMPLE_MAJOR, EXAMPLE_NAME, &example_fops);
    if (result < 0) {
        printk("<1>EXAMPLE: Failed to register character device\n");
        return result;
    }

    return 0;
}

值得一提的是第一個參數 EXAMPLE_MAJOR 可以是 60, 61, 62。如果是正式要釋出的 Driver,就必須要從 Documentation/devices.txt 選取適當的 Major ID。當然,在 example_exit() 我們也必需加上對應的 unregister:

static void example_exit(void) {
    printk("<1>EXAMPLE: exit\n");

    /* Unregister character device */
    unregister_chrdev(EXAMPLE_MAJOR, EXAMPLE_NAME);
}

在重新編譯之後,我們可以用 insmod 載入驅動程式,然後使用 mknod 建立 Device File。然後我們就可以在 User Space 使用一般的檔案讀寫操作這個 Device。

$ sudo insmod ./example.ko

$ sudo mknod /dev/example c 60 0
# /dev/example 是我們要存放檔案的路徑,c 代表 Character Device,60 是這個驅動程式的 Major ID,0 是驅動程式的 Minor ID。

$ sudo chmod 666 /dev/example
# 為了方便測試,我們把這個 Device 改成所有人都可以讀寫。

$ echo -n 'abcd' > /dev/example

$ sudo dmesg | tail

讀取 User Space 的資料


在前一節當中我們提供了一個 API 讓 User Space 可以操作 Driver。但是其實我們是不能直接存取 buf 的內容。因為 Kernel Space 與 User Space 有不同的位址空間,所以不能直接存取他們。我們必須借助 copy_from_user 這個 API。

在使用這個 API 之前,我們必需引入 <asm/uaccess.h>:

#include <asm/uaccess.h>

然後我們就可以使用 copy_from_user 來存取 User Space 的位址空間,舉例來說:

ssize_t example_write(struct file *filp, const char *buf, size_t size, loff_t *f_pos) {
    size_t pos;
    uint8_t byte;
    printk("<1>EXAMPLE: write  (size=%zu)\n", size);
    for (pos = 0; pos < size; ++pos) {
        if (copy_from_user(&byte, buf + pos, 1) != 0) {
            break;
        }
        printk("<1>EXAMPLE: write  (buf[%zu] = %02x)\n", pos, (unsigned)byte);
    }
    return pos;
}

值得注意的是 copy_from_user() 會回傳剩下未完成的 byte 數。所以一般來說這個回傳值必須是 0 才是成功地讀入資料。要把資料從 Kernel Space 複製到 User Space 則是使用 copy_to_user() 函式,至於使用方法就不再贅述。

$ echo -n 'abcd' > /dev/example
$ sudo dmesg | tail

小結


透過這個小練習,我們可以知道要怎麼開始寫一個 Linux Driver。在下一結我們會從 QEMU 的角度出發,建立一個 QEMU 的虛擬裝置,讓 QEMU Guest OS 的驅動程式可以和外面的 QEMU 虛擬裝置相互溝通。

參考資料

2011年10月23日 星期日

從 Android Open Source Project 下載 Android 原始碼

前一陣子因為受到 kernel.org 被駭的牽連,Android Open Source Project (AOSP) 的 git repository 也關閉了一段時間。大約二天之前 AOSP 回來了,不過網址有些許的變動,我就稍微記錄一下下載流程,以免日後忘記要怎麼弄。

先準備必要的工具


要下載 AOSP 的程式碼,你至少需要 curl、git、python 這三個程式。在 Debian 或 Ubuntu 上面你只要使用:

$ sudo apt-get install curl git python

就可以把它們弄到手。接下來我們要先設定 git:

$ git config --global user.name 你的名字
$ git config --global user.email 你的email

下載 repo 版本管理工具


接下來我們必需下載 repo 版本管理工具,我們可以在家目錄之下建立一個資料夾,並把這個資料夾加到 $PATH 裡面:

$ mkdir ~/bin
$ export PATH=~/bin:$PATH
# 備註:你可以把上面這行加到你的 .bashrc,這樣以後要用 repo 的時候就可以直接用。

然後下載 repo 這個工具:

$ curl https://dl-ssl.google.com/dl/googlesource/git-repo/repo > ~/bin/repo
$ chmod +x ~/bin/repo

如果你做到這裡沒有遇到問題就可以開始下載程式碼。如果你遇到 SSL certificate problem 之類的問題可以看下面的常見問題。

開始下載 Android 程式碼


接下來就是這篇文章的重點:下載 Android 程式碼。首先我們先建立一個用來放置 Android 程式碼的資料夾:

$ mkdir ~/android-src
$ cd ~/android-src

初始化 repo 相關的設定:

$ repo init -u https://android.googlesource.com/platform/manifest

接著 repo 會問你一些問題,通常使用預設值就可以了。最後就是按下:

$ repo sync
# 備註:你可以加上 -j2 或 -j4 平行下載。

之後經過漫長的等待,你就會有一份完整的 Android 程式碼

下一步:你可以參考另一篇文章「編譯 Android AOSP」編譯整個 Android 系統。



--------------------------------------------------

常見問題


為什麼會一直停在 Receiving Objects 且網路毫無動靜?


有時候可能因為 TCP/IP 的實作可能遇上一些意外的問題,所以會一直卡在「Receiving Objects」,可以用以下指令調整 TCP 協定的設定:

$ sudo sysctl -w net.ipv4.tcp_window_scaling=0

然後在 repo sync 的時候,只使用單執行緒:

$ repo sync -j1

為什麼會遇到 SSL certificate problem 之類的問題?


如果你遇到 SSL certificate problem 之類的問題,是因為你的系統提供的 curl 沒有內建 cacert 清冊 (也就是 https 憑證發行單位的清冊),curl 沒有辦法驗證 dl-ssl.google.com 的憑證是否正確。

你必需先行下載 cacert.pem。因為 cacert.pem 是防止中間人攻擊的重要機制,所以請務必要確保他的正確性,你可以使用 sha1sum 來檢查:

$ curl  http://curl.haxx.se/ca/cacert.pem --ipv4 > ~/cacert.pem
$ sha1sum ~/cacert.pem
286c4a22fe2ed9e3b2d958e2800a8c16d867f063  cacert.pem

然後再使用以下指令下載 repo 這個工具:

$ curl --cacert ~/cacert.pem https://dl-ssl.google.com/dl/googlesource/git-repo/repo > ~/bin/repo

另外,我們要讓 git 使用這個清冊驗證 Google 的 repository,所以我們要執行以下指令:

$ git config --global http.sslcainfo ~/cacert.pem

參考資料

  1. cURL CA Extract
  2. Android Open Source Project: Download the Source Tree

2011年9月13日 星期二

C++ Iterator 與 Lambda Function

今天寫程式碼的時候遇到一個問題:要怎麼讓一個存有指標的陣列依照指標指向的內容做排序呢?例如該怎麼作才可以讓下面的程式碼依序印出 1~7 呢?

int main() {
  int a[] = { 5, 4, 1, 2, 7, 3, 6 };

  vector<int *> b;
  b.push_back(&a[6]);
  b.push_back(&a[5]);
  b.push_back(&a[2]);
  b.push_back(&a[0]);
  b.push_back(&a[4]);
  b.push_back(&a[1]);
  b.push_back(&a[3]);

  for (size_t i = 0; i < b.size(); ++i) {
    cout << *b[i] << endl;
  }

  return EXIT_SUCCESS;
}

我們一開始可能會想要用 <algorithm> 裡面的 sort,不過很可惜,下面的程式碼一定是錯的:

#include <algorithm>

int main() {
  // ... 略 ...

  sort(b.begin(), b.end());

  for (size_t i = 0; i < b.size(); ++i) {
    cout << *b[i] << endl;
  }

  return EXIT_SUCCESS;
}

這是因為在這種情況下,sort 函式比較的是位址的大小,而不是每一個 Pointer 指向的 int 的值。而且令人遺憾的是我們不能重載 Pointer Type。當然最簡單的解決方法是:
struct int_ptr_less_than_comparator {
public:
  bool operator()(int const *pa, int const *pb) const {
    return *pa < *pb;
  }
};

int main() {
  // ... 略 ... 

  sort(b.begin(), b.end(), int_ptr_less_than_comparator());

  for (size_t i = 0; i < b.size(); ++i) {
    cout << *b[i] << endl;
  }
 
  return EXIT_SUCCESS;
}

我們直接寫一個比較函式 (函式物件),然後把它作為 sort 函式的第三個參數。這樣一來結果是對了,不過感覺可以寫得更為一般化。首先我們可以觀察到 int_ptr_less_than_comparator 是由二個部分組成:(1) 先對 pa 與 pb 取值 (dereference) (2) 比較二者的大小。其中,第二部分和 std::less 的功能是一樣的。而第一部分可以寫一個叫作 dereference_to 的函式物件來解決:

#include <functional>

template <typename T>
struct dereference_to : public std::unary_function<T *, T &> {
public:
  T &operator()(T *ptr) const {
    return *ptr;
  }
};


這個 dereference_to 看起來很複雜,不過不要被他嚇到了!它只是一個帶有 operator() 的 class template。舉例來說,如果我們把 int 代入 T:

struct dereference_to<int>
: public std::unary_function<int *, int &> {
public:
  int &operator()(int *ptr) const {
    return ptr;
  }
}

這樣如果我們忽略 std::unary_function,剩下的應該都不難理解,簡單的說,就是我們可以宣告一個 dereference_to<int> 物件,然後透過其 operator() 運算子幫我們 dereference operator() 的第一個參數。例如:

int main() {
  int c = 5;
  dereference_to<int> dref;
  cout << dref(&c) << endl; // 印出 5
}


所以我們現在有二種函式物件:std::less 與 dereference_to,可是我們還需要一個方法幫我們把 dereference_to 與 std::less 組裝成 int_ptr_less_than_comparator。如果你的編譯器附有非標準的 compose2,你就可以用以下程式碼「組合」出 int_ptr_less_than_comparator:

compose2(less<int>(), dereference_to<int>(), dereference_to<int>())

可是 compose2 畢竟不是標準的一部分,所以我們要自己寫一份:

template <typename F, typename G, typename H>
struct binary_composer
: public binary_function<typename G::argument_type,
                         typename H::argument_type,
                         typename F::result_type>
{
private:
  F f;
  G g;
  H h;

public:
  // 這些 typedef 是要讓 Type Traits 機制可以正常運作。一開始看不懂沒有關係。
  // 只要依樣畫葫蘆即可!
  typedef typename G::argument_type first_argument_type;
  typedef typename H::argument_type second_argument_type;
  typedef typename F::result_type result_type;

public:
  binary_composer(F const &f_, G const &g_, H const &h_)
    : f(f_), g(g_), h(h_) { }


  // 理論上這部分要 overload 四個版本 (不同的 const) 不過為了簡單起見我只寫了一個。
  result_type operator()(first_argument_type const &arg1,
                         second_argument_type const &arg2) const {
    return f(g(arg1), h(arg2));
  }
};

// 因為 class 的 constructor 是要在 class template 具現化之後才能被呼叫,所以我們
// 必須要寫一大串才可以產生 binary_composer 的 object。這很不方便,所以我們可以
// 再寫一個 function template,讓 C++ 的型別推導機制自動推導出來。
template <typename F, typename G, typename H>
binary_composer<F, G, H> compose2(F const &f, G const &g, H const &h) {
  return binary_composer<F, G, H>(f, g, h);
}
 
這樣萬事俱備,只要把它們組合起來就完成了!

int main() {
  // ... 略 ...

  sort(b.begin(), b.end(),
       compose2(less<int>(),
                dereference_to<int>(),
                dereference_to<int>()));

  for (size_t i = 0; i < b.size(); ++i) {
    cout << *b[i] << endl;
  }

  return EXIT_SUCCESS;
}

接下來我們把焦點放到用來印出數字的 for 迴圈,有沒有辦法寫得更為一般化呢?一般我們要印數字的時候,我們可以用 copy 加上 ostream_iterator 達成目標。不過這次 dereference_to 又為我們帶來一些麻煩!這次我們要寫 Output Iterator 的 Decorator:

template <typename I, typename D>
struct output_iterator_decorator
: public iterator<output_iterator_tag, typename D::argument_type> {
private:
  typedef output_iterator_decorator<I, D> THISCLASS;

private:
  I iterator;
  D decorator;

public:
  output_iterator_decorator(I const &iterator_, D const &decorator_)
  : iterator(iterator_), decorator(decorator_) {
  }

  THISCLASS &operator=(typename D::argument_type const &arg) {
    // 先呼叫 decorator 再把回傳值 assign 給 iterator
    *iterator = decorator(arg);
    return *this;
  }

  // dereference 運算子
  THISCLASS &operator*() {
    return *this;
  }

  // 前置遞增運算子
  THISCLASS &operator++() {
    ++iterator;
    return *this;
  }

  // 後置遞增運算子
  THISCLASS &operator++(int) {
    iterator++;
    return *this;
  }
};

// 和前面一樣,利用 C++ 的型別推導具現化 output_iterator_decorator
template <typename I, typename D>
output_iterator_decorator<I, D> decorate(I iter, D decorator) {
  return output_iterator_decorator<I, D>(iter, decorator);
}

有了以上的準備我們就可以把 for 迴圈變成:

  copy(b.begin(), b.end(),
       decorate(ostream_iterator<int>(cout, "\n"),
                dereference_to<int>()));

到此我們再回頭看一下完整的 main 函式:

int main() {
  int a[] = { 5, 4, 1, 2, 7, 3, 6 };

  vector<int *> b;
  b.push_back(&a[6]);
  b.push_back(&a[5]);
  b.push_back(&a[2]);
  b.push_back(&a[0]);
  b.push_back(&a[4]);
  b.push_back(&a[1]);
  b.push_back(&a[3]);

  sort(b.begin(), b.end(),
       compose2(less<int>(), dereference_to<int>(),
                             dereference_to<int>()));

  copy(b.begin(), b.end(),
       decorate(ostream_iterator<int>(cout, "\n"),
                dereference_to<int>()));

  return EXIT_SUCCESS;
}

看起來還是很不直觀,而且為了省幾行程式碼,結果寫了更多難懂的程式碼,好像有一點本末倒置。所以我們再更進一步!如果你可以用 Boost.Lambda 函式庫,你可以寫以下的程式碼:

#include <iostream>

#include <algorithm>
#include <vector>

#include <boost/lambda/lambda.hpp>

using namespace boost::lambda;
using namespace std;

int main() {
  int a[] = { 5, 4, 1, 2, 7, 3, 6 };

  vector<int *> b;
  b.push_back(&a[6]);
  b.push_back(&a[5]);
  b.push_back(&a[2]);
  b.push_back(&a[0]);
  b.push_back(&a[4]);
  b.push_back(&a[1]);
  b.push_back(&a[3]);

  // 排序 (以 int * 指向的值)
  sort(b.begin(), b.end(), ((*_1) < (*_2)));

  // 一個一個印出來
  for_each(b.begin(), b.end(), cout << *_1 << "\n");

  return EXIT_SUCCESS;
}

對!你沒有看錯!這樣就沒有了!這裡的 _1 與 _2 是神秘的 Function Object,Boost.Lambda 函式庫會自動幫你組合成一個合適的 Function Object。(*_1) 就像是我們的 dereference_to,而 ((*_1) < (*_2)) 就像是我們的 int_ptr_less_than_comparator。至於印出數字的部分,我另外做了一些改動。所以不能使用 copy,而是要改成 for_each。

Boost.Lambda 函式庫雖然看起來很漂亮,不過它本身是用很複雜的技巧寫出來的。正常運作的時候很漂亮,不過寫出 bug 的時候,編譯器會噴很多錯誤訊息,一般人根本不可能看得懂。某種意義上來說這已經是函式庫的極限了!

最新的 C++11 加入了 Lambda function (就是下面粗體字的部分),我們再也不需要 Boost.Lambda 也可以達到類似的效果:

#include <iostream>

#include <algorithm>
#include <vector>

using namespace std;

int main() {
  int a[] = { 5, 4, 1, 2, 7, 3, 6 };
 
  vector<int *> b; 
  b.push_back(&a[6]);
  b.push_back(&a[5]);
  b.push_back(&a[2]);
  b.push_back(&a[0]);
  b.push_back(&a[4]);
  b.push_back(&a[1]);
  b.push_back(&a[3]);

  // 排序 (以 int * 指向的值)
  sort(b.begin(), b.end(), [](int *pa, int *pb) { return *pa < *pb; });

  // 一個一個印出來
  for_each(b.begin(), b.end(), [](int *p) { cout << *p << "\n"; });

  return EXIT_SUCCESS;
}

讓我們期待 C++11 的到來吧!

--
備註:範例程式碼 http://www.csie.ntu.edu.tw/~b97073/B/iterator.tgz

2011年8月8日 星期一

處理 Stack Overflow

直接看程式碼比較快:

#define _XOPEN_SOURCE 600

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <signal.h>

sigjmp_buf stack_overflow_guard;

char alter_stack[4096];

void sigsegv_handler(int sig) {
  if (sig == SIGSEGV) {
    /* Jump to catch block */
    siglongjmp(stack_overflow_guard, sig);
  }
}

void install_sigsegv_handler() {
  /* Prepare alternating stack */
  stack_t stk;

  stk.ss_sp = alter_stack;
  stk.ss_size = sizeof(alter_stack);
  stk.ss_flags = 0;

  if (sigaltstack(&stk, NULL) == -1) {
    fprintf(stderr, "ERROR: Unable to setup alternative stack.\n");
    exit(EXIT_FAILURE);
  }

  /* Register SIGSEGV handler */
  struct sigaction act;

  act.sa_handler = &sigsegv_handler;
  sigfillset(&act.sa_mask);
  act.sa_flags |= SA_ONSTACK;

  if (sigaction(SIGSEGV, &act, NULL) == -1) {
    fprintf(stderr, "ERROR: Unable to instal signal handler.\n");
    exit(EXIT_FAILURE);
  }
}

int trigger_stack_overflow(int a, int b, int c, int d, int e) {
  return trigger_stack_overflow(a + 1, b + 1, c + 1, d + 1, e + 1) *
         trigger_stack_overflow(a - 1, b - 1, c - 1, d - 1, e - 1);
}

int main() {
  install_sigsegv_handler();

  if (sigsetjmp(stack_overflow_guard, 0) == 0) {
    printf("normal path, got: %d\n",
           trigger_stack_overflow(0, 0, 0, 0, 0));
  } else {
    printf("stack overflow\n");
  }

  return EXIT_SUCCESS;
}

2011年5月20日 星期五

LaTeX 與 Beamer

最近因為一些需要,必需使用 LaTeX 製作投影片,所以研究了一下 Beamer 的使用方法。其實寫起來並不困難,和一般的 LaTeX article 差不多。Beamer 的 document structure 如下:

\documentclass{beamer}
\usetheme{Singapore} %% 這一個是佈景主題 (選用)
\begin{document}

\title{Title}
\author{Author Name}
\institute{Institute}

\frame{\titlepage} %% 顯示標題頁面

%% ... 其他內容 ...

\end{document}

接下來和 article 一樣,可以分為 section 與 subsection,而每一個 section 和 subsection 可以由若干個 frame 組成。

\section{Section 1}
%% 如果你有需要,可以在這裡加上 \subsection{Subsection 1.1} 
\begin{frame}
  \frametitle{Frame 1.1}
  \begin{itemize}
    \item{Item 1}
    \item{Item 2}
  \end{itemize}
\end{frame}

我們可以看到每個 frame 都會有一個 frametitle,也就是每一頁上方的標題。而下面就是該頁的內容。如果你要條列出各個要點,可以使用 itemize。

最後如果你需要顯示大綱,可以在 titlepage 下面加上:

\section*{Outlines}
\begin{frame}
  \frametitle{Outlines}
  \tableofcontents
\end{frame}

不過因為要建立 tableofcontents 所以要執行 pdflatex 編譯你的投影片二次。這樣你就可以得到一個精美的投影片了。你可以在這裡下載到範例投影片與原始檔

2011年4月30日 星期六

用 OpenMP 實作平行化的 Quicksort

OpenMP 和 Pthread 不同,我們沒有辦法很精確地控制有多少的 Thread 正在執行。所以如果要實作 Quicksort,我們不能直接使用 Recursion。相反地,我們必須引入 Threading Pool 的概念,讓各個 Thread 主動去某個工作清單拿工作。概念程式碼如下:

void quicksort_impl(sort_value_t array[],
                    stack_t *s,
                    size_t *busy_threads) {
  int idle = 1;

  size_t begin = 0;
  size_t end = 0;

  /* We should loop forever until everyone is idle */
  while (1) {
    while (idle) {
      /* This thread is idling.
         Try to get a new task from the stack. */

#pragma omp critical
      if (!stack_empty(s)) {
        /* Get the pending sort range, start work now! */
        sort_range_t *range = stack_top(s);
        begin = range->begin;
        end = range->end;
        stack_pop(s);

        ++*busy_threads;
        idle = 0;
      }

      /* If everyone finished his work, then leave now ... */
      if (*busy_threads == 0) {
        return;
      }
    }

    size_t size = end - begin;

    if (size <= 50) {
      sort_value_t *seg = array + begin;

      /* Use backward insertion sort when the sort range is small */
      size_t i, j;
      for (i = 1; i < size; ++i) {
        sort_value_t cur = seg[i];
        /* Move the number backward if they are greater than cur */
        for (j = i; j > 0 && seg[j - 1] > cur; --j) {
          seg[j] = seg[j - 1];
        }
        seg[j] = cur;
      }

      /* Mark begin as end so that we can acquire next sort range */
      begin = end;

#pragma omp critical
      --*busy_threads;

      idle = 1;

      /* Finished our range continue to acquire next range */
      continue;
    }


    /* Partition our range */
    sort_value_t *seg = array + begin;

    size_t pivot_index = rand() % size;
    SWAP(seg[0], seg[pivot_index]);
    sort_value_t pivot = seg[0];

    size_t left = 1;
    size_t right = size - 1;

    while (left < right) {
      while (seg[left] <= pivot && left < right) { left++; }
      while (seg[right] >= pivot && left < right) { right--; }
      SWAP(seg[left], seg[right]);
    }

    if (seg[left] > pivot) {
      left--;
    }
    SWAP(seg[0], seg[left]);

    /* Push left subrange to stack */
#pragma omp critical
    {
      sort_range_t range;
      range.begin = begin;
      range.end = begin + left;
      stack_push(s, &range);
    }

    /* Continue to sort right subrange */
    begin += left + 1;
  }
}

void quicksort(sort_value_t array[], size_t size) {
  size_t busy_threads = 0;
  stack_t *s = stack_new();
  sort_range_t all = { 0 , size };
  stack_push(s, &all);

  /* Run quicksort_impl in parallel */
#pragma omp parallel
  quicksort_impl(array, s, &busy_threads);

  stack_delete(s);
}

2011年4月13日 星期三

Haskell 與 State Monad

昨天試著用 HaskellState Monad,花了一點時間才寫出一個簡單的猜數字遊戲。感覺要在 Functional Programming Language 寫出 State 的概念比較困難...

import Control.Monad.State
import System.IO
import System.Random

gameLB, gameUB :: Integer
gameLB = 0
gameUB = 100

data GameState = GameState { steps :: Integer ,
                             lb :: Integer,
                             ub :: Integer }

initialState :: Integer -> Integer -> GameState
initialState l u = GameState { steps = 0, lb = l, ub = u }

updateState :: Integer -> Integer -> GameState -> GameState
updateState ans guess (GameState { steps = s, lb = l, ub = u }) =
    case (compare ans guess) of
      EQ -> GameState { steps = s + 1, lb = ans, ub = ans }
      LT -> GameState { steps = s + 1, lb = l, ub = min (guess - 1) u }
      GT -> GameState { steps = s + 1, lb = max l (guess + 1), ub = u }

range :: GameState -> (Integer, Integer)
range (GameState { lb = l, ub = u }) = (l , u)

stepCount :: GameState -> Integer
stepCount (GameState { steps = s }) = s

gameSession :: Integer -> StateT GameState IO Integer
gameSession ans =
    do (l, u) <- gets range
       lift $ putStr $ "Guess number (" ++ show l ++ ".." ++ show u ++ "): "
       lift $ hFlush stdout
       str <- lift $ getLine
       let num = read str
       modify (updateState ans num)
       case (compare ans num) of
         EQ -> gets stepCount >>= return
         LT -> gameSession ans >>= return
         GT -> gameSession ans >>= return

main :: IO ()
main = do rand <- getStdRandom (randomR (gameLB, gameUB))
          s <- evalStateT (gameSession rand) (initialState gameLB gameUB)
          putStrLn $ "Steps used: " ++ show s

備註:上面的程式用 ghc --make Guess.hs 就可以編譯了(有用到 mtl package

2011年3月12日 星期六

libffi 的範例

在 C/C++ 當中,函式的型別必需在編譯期固定下來才可以正確的呼叫。所以 Function Pointer 都要記錄 Function 的 Signature,簡單地說下面二個 Function Pointer 是不同的型別:

void (*func0)();
void (*func1)(int);

然而在很多情況下,我們只能在執行期決定我們要呼叫的函數。最常見的例子就是有提供 Native Function Call 的 Interpreter。例如:Python (C API)、Java (JNI) 等等。然而這種功能如果只能使用 C 語言實作,會變得非常龐雜、沒效率而且失去一般性。所以有了 libffi 的產生。

libffi 的全名是 Foreign Function Interface,他本身是用組合語言撰寫,不過他提供了一個 Portable 的 API。我們只要呼叫這些 API 就可以進行動態的函式呼叫(這和 Function Pointer 不同。如前所述,Function Pointer 必需在 Compiler Type 決定函式的型別。而 libffi 可以在執行期決定 Callee 的型別)。許多有名的計劃都是 libffi 的使用者,例如:CPythonOpenJDKDalvik VM 等等。

以下是簡單的範例程式:

#include <stdio.h>
#include <stdlib.h>

/* libffi 的 header */
#include <ffi.h>

/* 一些亂七八糟的函式 */
int func0() {
  printf("%s()\n", __func__);
  return 0;
}

int func1(int p0) {
  printf("%s(%d)\n", __func__, p0);
  return p0;
}

int func2(int p0, int p1) {
  printf("%s(%d, %d)\n", __func__, p0, p1);
  return p0 + p1;
}

/* 函式查詢表。也可以使用 hash table、binary tree 來實作,
   不過作為一個簡單的範例直接使用 array 就好了。 */
void *func_table[] = {
  (void *)func0, (void *)func1, (void *)func2,
};

int main(int argc, char **argv) {
  int i;

  /* 決定我們要使用哪一個函式 */
  if (argc < 2) {
    fprintf(stderr, "USAGE: %s parameter-count\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  int parameter_count = atoi(argv[1]);

  if (parameter_count > 2) {
    fprintf(stderr, "WARNING: Parameter count is greater than 2.\n");
    parameter_count = 2;
  }

  /* 讀取函式的參數 */
  int values[2] = { 0 , 0 };

  for (i = 0; i < parameter_count; ++i) {
    if (scanf("%d", &values[i]) != 1) {
      static char const *const postfix[] = {
        "th", "st", "nd", "rd", "th",
        "th", "th", "th", "th", "th"
      };

      fprintf(stderr, "Unable to read %d%s parameter\n",
              i + 1, postfix[(i + 1) % 10]);
      exit(EXIT_FAILURE);
    }
  }

  /* 使用 libffi 呼叫指定的函式 */
  ffi_cif cif;
  ffi_type *args[] = { &ffi_type_sint, &ffi_type_sint };
  void *ptr_values[] = { &values[0], &values[1] };

  /* 準備 ffi 所需的資料結構 */
  if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, parameter_count,
                   &ffi_type_sint, args) == FFI_OK) {
    int sum = 0;

    /* 執行函式呼叫 */
    ffi_call(&cif, func_table[parameter_count], &sum, ptr_values);

    printf("return value: %d\n", sum);
  } else {
    fprintf(stderr, "Unable to initialize FFI structure.\n");
    exit(EXIT_FAILURE);
  }

  return EXIT_SUCCESS;
}

2011年2月12日 星期六

幫 C 語言加上 Garbage Collector

前幾天讀 Dragon Book 的時候突發奇想,何不自己實作 Conservative Garbage Collector?

一般來說 Tracing Garbage Collector 分成以下三個步驟:
  1. 先從 call stack 或者 global variables 的 references (或者 pointers) 蒐集 object 的位址作為 root set,並標記 root set 當中的 object。
  2. 檢視 root set 當中的 object。如果 object 也包含 reference,而且這個 reference 指向的 object' 沒有被標記過,就標記並檢視 object'。(不斷遞迴直到沒有新的 object 被標記)
  3. 清除並回收沒有被標記的 object。
然而為了有效率地計算 root set 與走訪被標記的 objects,通常我們會需要編譯器加入一些額外的資訊。而且程式語言必需是 type-safe 的!著名的例子如 Java 或者是 Haskell、Common Lisp 等等。而 C/C++ 就不適合當 Garbage Collector 的目標語言,因為 C/C++ 支援 Pointer Arithmetic、Union Type 等語言構件,使得我們幾乎不可能在 C/C++ 之上實作 (Accurate) Garbage Collector。

不過 Conservative Garbage Collector 的做法不太一樣。Conservative Garbage Collector 的想法是:我不去分辨誰是 Pointer Type 誰是 Integer Type,只要「看起來」像是 Pointer 的 bit pattern,我就把他當成 Pointer。這有一個好處:我們不需要編譯器的幫忙也可以寫 Garbage Collector,所以可以和其他的程式碼混用;然而也有一個壞處:有時候 Conservative Garbage Collector 過於保守,以至於無法回收一些可以回收的記憶體。以下我就以一個小程式展現 Conservative Garbage Collector 的基本概念。

首先我們的第一個問題是如何找到 root set?我們可以取 main 函式 argc 的位址,另外在 gc_cleanup_ 函式隨意傳入一個整數,再取該整數的位址。所有還可能會用到的 pointer 就會在這二個整數之間:

void *stack_top;

void gc_init(int *argc_ptr) {
  stack_top = argc_ptr;
}

void gc_cleanup_(int stack_indicator) {
  void *stack_bottom = &stack_indicator; 
  /* Stack 的有效範圍就介於 [stack_bottom, stack_top] 之間 */
}

int main(int argc, char **argv) {
  gc_init(&argc);
}

當然我們還必需考慮 alignment 的問題,所以要把 stack_top 與 stack_bottom 對齊到 sizeof(void *) 的某倍。這可以用以下二個巨集來達成:

#define FLOOR_ALIGN_TO_WORD(X) \
    (((uintptr_t)(X)) / sizeof(void *) * sizeof(void *))

#define CEIL_ALIGN_TO_WORD(X) \
    (((uintptr_t)(X) + sizeof(void *) - 1) / \
        sizeof(void *) * sizeof(void *))

接著我們要準備一些空間來當我們的 Heap,所以我們在 gc_init 之中加入一些程式碼:

static char *heap_begin = NULL;
static char *heap_free = NULL;
static char *heap_end = NULL;

/* Initialize the internal data structure of the
   garbage collector */
void gc_init(int *argc_ptr) {
    /* We assume that argc is the variable in the
       stack having the highest address. */
    stack_top = (void **)CEIL_ALIGN_TO_WORD(argc_ptr);

    /* Allocate the heap using mmap. */
    heap_begin = (char *)mmap(0, GC_HEAP_SIZE,
                              PROT_READ | PROT_WRITE,
                              MAP_PRIVATE | MAP_ANONYMOUS,
                              -1, 0);

    if (!heap_begin || heap_begin == MAP_FAILED) {
        fprintf(stderr,
                "Unable to allocate the heap (size=%lu).\n",
                (unsigned long)GC_HEAP_SIZE);
        exit(EXIT_FAILURE);
    }

    heap_free = heap_begin;
    heap_end = heap_begin + GC_HEAP_SIZE;
}

接著我們需要一些資料結構用以管理我們分配出去的記憶體。做為一個簡單的範例,我只用了一個陣列依序記錄分配出去的記憶體,我的陣列會從 heap_end 開始往 heap_begin 的方向生長。

當然這是一個很沒有效率的作法,比較有效率的方法是使用類似 buddy system 之類的資料結構來管理 heap。

enum {
    ALLOC_STATUS_UNKNOWN,
    ALLOC_STATUS_TOUCHED,
    ALLOC_STATUS_REFERRED,
    ALLOC_STATUS_DEALLOCATED,
};

typedef struct alloc_record_ {
    char *addr;
    size_t size;
    unsigned int status;
} alloc_record;

為了操作 alloc_record 資料結構,我寫了三個函式,其功能分述如下(程式碼就不再贅述):

static alloc_record *find_alloc_record(char *addr) {
  /* 給定一個位址,找出它是屬於哪一次 allocation */
}

static void insert_alloc_record(char *addr, size_t size) {
  /* 在 alloc_record 的尾端加上一筆記錄 */
}

static void *allocate_deallocated_block(size_t size) {
  /* 重新把 deallocated 過的位址配置給不同的人 */
}

接下來是 Malloc 的程式碼。我們先嘗試直接從 heap_free 配置記憶體,如果記憶體不夠,再嘗試使用別人 deallocate 的記憶體。如果以上二者都沒有用,我們就必需進行 Garbage Collection。(備註:gc_cleanup(); 即 gc_cleanup_(0);)

/* Allocate a block with given bytes. */
void *gc_malloc(size_t size) {
    /* Make sure that all of returned address is aligned */
    size = CEIL_ALIGN_TO_WORD(size);

    /* Try to allocate if free space available */
    if (is_free_space_avail(size)) {
        char *result = heap_free;
        heap_free += size;
        insert_alloc_record(result, size);
        return result;
    }

    /* Try to allocate from deallocated address. */
    void *result = allocate_deallocated_block(size);
    if (result) {
        return result;
    }

    /* Try to collect garbage and retry. */
    while (gc_cleanup()) {
        void *result = allocate_deallocated_block(size);
        if (result) {
            return result;
        }
    }

    return NULL;
}

當然有 Malloc 就要有 Free,不過值得注意的是:為了提高 Conservative Garbage Collector 的回收率,減少「應該 deallocate 而沒有 deallocate」的情況,我們應該要抹除整個 object 與傳入的 Pointer

/* Deallocate and wipe the allocated block. */
static void deallocate(alloc_record *record) {
    /* Mark this block as deallocated */
    record->status = ALLOC_STATUS_DEALLOCATED;

    /* Wipe the memory block for cleaning the possible
       pointer address */
    memset(record->addr, '\0', record->size);
}

/* Deallocate the given address and wipe the pointer. */
void gc_free_(void **addr_ptr) {
    alloc_record *record = find_alloc_record(*addr_ptr);
    if (!record) {
        fprintf(stderr, "Memory corrupted\n");
        return;
    }

    deallocate(record);
    *addr_ptr = NULL;
}

最後就是最關鍵的 Garbage Collection 的程式碼 gc_cleanup

/* Garbage collection and deallocate unused memory. */
size_t gc_cleanup_(int stack_indicator) {
    alloc_record *records =
        (alloc_record *)heap_end - alloc_record_count;

    size_t i, j;

    /* Mark alloc record as UNKNOWN */
    for (i = 0; i < alloc_record_count; ++i) {
        if (records[i].status != ALLOC_STATUS_DEALLOCATED) {
            records[i].status = ALLOC_STATUS_UNKNOWN;
        }
    }

    /* Scan the stack */
    void **stack_bottom =
        (void **)CEIL_ALIGN_TO_WORD(&stack_indicator);
    assert(stack_top != NULL);
    assert(stack_bottom <= stack_top);
    scan_and_touch(stack_bottom, (void **)stack_top);

    /* Scan the heap */
    scan_touched_objects();

    /* Deallocate and reset the status */
    static unsigned int gc_count = 0;
    static char const sep[] =
    "-------------------------------------------------------";

    fprintf(stderr, "%s\n", sep);
    fprintf(stderr,
            "GARBAGE COLLECTION ROUND #%u\n", ++gc_count);

    size_t deallocated_count = 0;
    for (j = alloc_record_count, i = j - 1; j > 0; --i, --j) {
        if (records[i].status == ALLOC_STATUS_UNKNOWN) {
            fprintf(stderr,
                    "  Reclaim [addr: %p, size: %lu]\n",
                    records[i].addr,
                    (unsigned long)records[i].size);

            deallocate(&records[i]);
            ++deallocated_count;
        }
    }

    fprintf(stderr, "%s\n\n", sep);

    return deallocated_count;
}

/* Scan for address between [begin, end) */
static void scan_and_touch(void *begin_, void *end_) {
    void **begin = (void **)FLOOR_ALIGN_TO_WORD(begin_);
    void **end = (void **)FLOOR_ALIGN_TO_WORD(end_);

    for (; begin < end; ++begin) {
        char *addr = (char *)*begin;

        if (addr >= heap_begin && addr < heap_free) {
            alloc_record *record = find_alloc_record(addr);

            if (record && record->status ==
                    ALLOC_STATUS_UNKNOWN) {
                record->status = ALLOC_STATUS_TOUCHED;
            }
        }
    }
}

/* Scan the touched objects */
static void scan_touched_objects() {
    alloc_record *records =
        (alloc_record *)heap_end - alloc_record_count;

    size_t count, i;

    do {
        count = 0;
        for (i = 0; i < alloc_record_count; ++i) {
            if (records[i].status != ALLOC_STATUS_TOUCHED) {
                /* Not in touched state, skip it. */
                continue;
            }

            /* Mark as referred. */
            records[i].status = ALLOC_STATUS_REFERRED;

            /* Scan this object */
            scan_and_touch(
                (void **)(records[i].addr),
                (void **)(records[i].addr + records[i].size));

            count++;
        }
    } while (count > 0);
}

以上程式碼就是一個具體而微的 Conservative Garbage Collector,完整的程式碼與測試程式可以從這裡下載:conservativegc.h , conservativegc.c , test_tree.c , test_many.c 。當然這個 Conservative Garbage Collector 還少了很多東西:包括計算 static storage 的 root set(在 Linux 之下可以檢查 etext 與 end 二者之間的值),還有更有效率的 allocation policy 等等。

備註:如果你真的有在 C/C++ 使用 Garbage Collector 的需求可以參考 libgc,一個由 BoehmDemersWeiser 等前輩撰寫 (他們是提出 Conservative Garbage Collector 的重要前輩),並被移植到多個平台的 Conservative Garbage Collector 函式庫。

2011年1月25日 星期二

Rvalue Reference 與 String 的實作

Rvalue ReferenceC++0x 當中,一個非常重要的新功能。有了 Rvalue Reference,我們就可以容易地寫出具有 Moving Semantics 的 class。如果善用它,我們可以寫出更有效率的 C++ 程式碼。目前比較有名的編譯器如 GCC 或者 Visual C++ 都已經有 Rvalue Reference。

說來說去,Rvalue Reference 究竟是什麼?

首先我們必須要介紹 Lvalue 與 Rvalue。C++ 程式語言之中,Rvalue 與 Lvalue 相反的概念,Rvalue 的定義是「不是 Lvalue 的 Object(或曰 Variable),就是 Rvalue」。那 Lvalue 又是什麼?所謂的 Lvalue 是指在記憶體上有固定位置可以取址,可以放在指派運算子左邊的值。例如:

int a, b, c;
a = 1; b = 2; c = 3;  // (1)
a = b + c;  // (2)

第一行當中 a、b、c 三個變數都是放在指派運算子的左邊,所以都是 Lvalue。而第二行當中,a 是 Lvalue,(b+c) 這個表達式是 Rvalue。下面的程式很明顯是不合法的:

b+c = 0; // X

因為 b+c 這個 Rvalue 在記憶體上沒有固定的位址(翻譯為機器碼後,b+c 通常會被儲存在暫存器),所以自然也不能把 0 指派給他。但是這和 Rvalue Reference 有什麼關係呢?我們看看這個例子:

string a("abc");
string b("def");
string c("ghi");

string all = a + b + c + a;  // (3)

第三行的 (a+b) 、((a+b)+c)、(((a+b)+c)+a) 都是 Rvalue。在 C++98 當中,我們只能把 Rvalue 綁定到 const 修飾過的 Reference,所以我們的 operator+ 通常會這樣寫:

string operator+(string const &lhs, string const &rhs)  {
  string result(lhs); // (4) copy lhs
  result += rhs; // (5) concat rhs
  return result;
}

每當我們呼叫 operator+,我們就要複製一份 lhs,然後再把 rhs 串接上去。我們仔細地看一下上述程式的執行過程:
  1. 首先執行 a+b 的時候,我們要先複製一份 a(稱為 result1),再把 b 串接到 result1 的後面,最後回傳 result1。
  2. 接著我們要計算 result1+c 的結果。我們必需先複製一份 result1(稱之為 result2),再把 c 串接到 result2 的後面,最後回傳 result2。
  3. 最後我們要計算 result2+a 的結果,我們必需要先複製一份 result2(稱之為 result3),再把 a 串接到 result3 的後面,最後回傳 result3。
這個程式非常沒有效率。它會複製一份 result1,一份 result2,可是複製完就忘掉 result1 與 result2!為了減少這樣的浪費,有人提出了 Move Semantics:

struct string_tmp {
  string *ptr;
  string_tmp(string *s) : ptr(s) { }
  ~string_tmp() { delete ptr; }
};

// Move Constructor
string::string(string_tmp rhs) {
  internal_move(rhs); 
}

// operator=
string &string::operator=(string_tmp rhs) {
  internal_move(rhs);
  return *this;
}

void string::internal_move(string_tmp with) {
  if (buf) {
    delete [] buf;
  }

  buf = with.ptr->buf;
  buf_size = with.ptr->buf_size;
  str_length = with.ptr->str_length;

  with.ptr->buf = NULL;
  with.ptr->buf_size = 0;
  with.ptr->str_length = 0; 
}

string_tmp operator+(string const &lhs, string const &rhs) {
  string_tmp result(new string(lhs));
  *(result.ptr) += rhs;
  return result;
}

string_tmp operator+(string_tmp lhs, string const &rhs) {
  *(lhs.ptr) += rhs;
  return lhs;
}

上面的程式碼最重要的概念:operator+ 回傳的型別變成 string_tmp。然後多載 operator+ 與 operator=。這二個 operator 如果看到 string_tmp,就會想辦法把裡面的東西偷出來用,從而避免無謂的複製!

雖然在 C++98 當中,我們可以模仿出 Move Semantics,不過寫起來即為痛苦,不但需要黑魔法(上面的程式碼並不完整,詳情請參閱 moving_string.hpp),而且難以維護,如果 C++ 可以讓我們把 Rvalue 偷出來用就好了!

這就是 Rvalue Reference 可以讓我們做的事!

Rvalue Reference 在 C++ 的 notation 如下:

T &&rref = ... ;

我們可以把 Rvalue 綁定到 Rvalue Reference 上,例如:

string &&rref = a + b;

但是我們沒有辦法直接把 Lvalue 綁定到 Rvalue Reference 上,下面這個 statement 是不合法的:

string a;
string &&rref = a; // X

這是為了防止我們錯誤地把 Lvalue 當成 Rvalue。還記得嗎?Lvalue 是在記憶體上有特定位置,程序員碰得到 Lvalue,因此我們不能偷  Lvalue 的東西。如果我想告訴編譯器:「沒關係,這個變數我不在乎你去偷東西,你把它當成 Rvalue 就可以了!」可以使用 std::move:

string &&rref = std::move(a);

說了這麼多,到底要怎麼用 Rvalue Reference?和前面的 string_tmp 一樣,我們必需要多載 operator+、operator=、move constructor:

// Move Constructor
string::string(string &&rhs)
  : buf(NULL), buf_size(0), str_length(0) {

  internal_move(rhs);
}

// operator=
string &string::operator=(string &&rhs) {
  if (this == &rhs) {
    return *this;
  }
  internal_move(rhs);
  return *this;
}

void string::internal_move(string &with) throw () {
  if (buf) {
    delete [] buf;
  }

  buf = with.buf;
  buf_size = with.buf_size;
  str_length = with.str_length;

  with.buf = nullptr;
  with.buf_size = 0;
  with.str_length = 0;
}

// operator+
string &&operator+(string &&lhs, string const &rhs) {
  lhs += rhs;
  return std::move(lhs); // convert lhs to r-value again
}

我們可以注意到:我們不再需要 string_tmp,編譯器可以幫我們分辨何者是 Rvalue,並告訴我們可不可以從該物件偷取資源!

結語:有了 Rvalue Reference,我們可以寫出更有效率的程式碼。到了 C++0x 的時代,一個好的 C++ 程序員應該要能掌握 Rvalue Reference 的威力!

完整的程式碼:test.cppsimple_string.hpp(一般的字串)、moving_string.hpp(有 move semantics 的字串)、rrefopt_string.hpp(使用 rvalue reference 的字串)。

2010年12月3日 星期五

合併 Git 的 Commit

Git 是一個很強大的分散式版本管理系統,因為有了 git,我習慣寫二三十行 code 就 commit 到 local repository。然而如此一來,repository 會充滿一大堆不能正常 compile 的 commit,而且會有一大堆 Fix Typo 之類很沒有用的 commit。有時候會想要把一些 commits 合併之後再上傳給別人,這時候就可以使用 rebase 來合併修改計錄。

(不過以下的流程只適合在 local repository 做,一旦上傳到 public repository,就不適合再 rebase 了!)

首先我們要知道過去的 commit 記錄:

$ git log  # 先看每一個 commit 的 log 與 sha1 編號

假設我們要把 branch1 的  commitA..commitB 的 commits 合併起來,我們就這樣做(以下的 commitA 與 commitB 皆是指這二個 commit 的 sha1 編號):

$ git checkout commitB  # 先回到過去某個 commit

$ git reset --soft commitA  # 修改 working directory 的 commit index

$ git commit --amend  # 更新 commit 訊息與時間 (合併 commitA..commitB)

$ git tag tmp  # 記錄合併後的 commit 的 sha1 編號(你直接記 sha1 編號也是可以)

$ git checkout branch1  # 回到原本 branch 的 HEAD

$ git rebase --onto tmp commitB
# 把 commitB 之前的 commit history 換成合併之後的 commit。

$ git tag -d tmp  # 刪除暫時的 tag
--
資料來源:
How do I combine the first two commits of a Git repository?

2010年7月22日 星期四

範文?駢體文?八股文?

前日,學測公佈了一篇得滿分的作文,評語是「感情抒發非常有張力,組織結構、起承轉合也很好...」

可是我自己看這一篇文章的時候覺得這一篇文篇華而不實,空有華麗的修辭,然而打動人心的部分甚至比朱自清的背影還少的很多。當然,這位同學的寫作水平很高應該是事實(但有一種補習班教出來的感覺),拿滿分應受之無愧,但是我不太能接受這樣的文章被選為範文。

我記得我的高中老師曾經印了全國寫作比賽第一名的文章給我們看,我和一些同學都認為這篇文章雖然寫得很美,但應該也算不上什麼好文章。不過事實上老師別有用心,在第一名作文的背面,老師印了另一篇社論:為什麼第一名的文章雖有優美的修辭、嚴謹的結構,然而卻有「為賦新辭強說愁」的感覺,明明才十七八歲,就有歲月不待人之類的感嘆...?

我的國文一向不好,不過高中課文中,我還蠻喜歡劉勰的《文心雕龍》〈情采〉。這一篇提到:「是以衣錦褧衣,惡文太章;賁象窮白,貴乎反本。」我自己讀到這一段是深表認同的!我讀的文學類書籍雖然不多,然而所有讓我感動過的文章,都沒有這種誇張到不行的修辭。不過我們閱卷老師似乎很吃這一套,還選出來當範文,這不是另一種駢體文嗎?我們的韓愈什麼時候才能出現呢?

唉... ,除了大環境我沒啥好說。

2010年7月14日 星期三

FLOLAC 2010 感想

我最早是從柯學長那裡聽說 FLOLAC 暑期學分班的,根據柯學長的說法 FLOLAC 給他很大的啟發,所以今年我也去報名,想要想想「程式言語」道底是長什麼樣子?經過了二個星期,我覺得確實大開眼界看到了很多不一樣的東西!以下我就分別介紹這二個星期的課程:
  • Logic 邏輯 - 在這個課程之內,我學到了 Propositional Logic 與 Calculational Logic,還有 Functional Complete 的概念,與 Functional Complete 的證明(事實上,Sum of Product 應該只可以當結果,我們要證 Functional Completeness 要先確認 Propositional Letter 再做 Structural Induction)。除此之外,我還學到了 First Order Logic、Sequent Calculus、Peano Axioms 與 First-order Logic 能力的極限等等很有趣而且過去沒有聽過的東西。
  • Functional Programming 函數編程 - 這應該是眾課程當中,我比較熟悉的一個。不過這一門課是教 OCaml (而不是我比較熟悉的 Haskell)。老師就從 Binding 的概念開始,一路介紹到怎麼寫一個函數(遞迴的),還有 High-order Function。另外,還有介紹到 OCaml 之中比較特別的 Parameterized Modules。所謂的 Parameterized Modules 是一個比較特別的 Module,只要你代入幾個參數,Parameterized Modules 會生產出一個新的 Module。我在上這一段的時候,心底想的是 C++ Template 與 Meta-programming。其實感覺起來還蠻像的!XD 不過這一門課的比重偏低,是比較可惜的地方!Folding/Unfolding 的部分就只有快速帶過而己,有一點可惜。
  • Operational Semantics 操作語意 - 這個課程是從「操作」的角度來向我們介紹語議。這樣講有一點抽象,簡單地說就是告訴我們要怎麼做才可以達到某個語意。例如:If、While、Assign 等等。這個課程主要又分成二個部分:Natural Semantics 與 Structural Operational Semantics。前者專注於大的概念,所以在 Composited Statement 當中,我們不會考慮比較小的 Statement 的情況,然而在 Structural Operational Semantics 我們會考慮不少細微的變化,包括平行程式執行起來可能會產生的差異等等。
  • Program Construction and Reasoning 程式建構與推理 - 這一門課是在教我們怎麼寫一個正確的程式,並且在建構一個程式的同時證明其正確性。在這一堂課當中,我們會用到 Hoare Logic。透過 Precondition、Postcondition、Loop Invariant 還有一些 Rules 讓我們可以知道程式是不是正確的。另外,穆老師還提到了 Goto Considered Harmful 的大論戰等等很有趣的東西!
  • Frama-C - Frama-C 是一套可以幫助我們驗證 C 程式寫得對不對的軟體。不過 Frama-C 本身沒有那麼厲害,有時候我們還是必須要加入一些參數,讓參數引導證明!在 Frama-C 這一堂課就會用到 Hoare Logic 與 Loop Invariant 的概念。不同之處在於我們會親自驗證別人寫得程式!所有我們用來練習的程式碼都是取材自開放原始碼的程式,而且真槍實彈地去找 bug。不過我自己覺得要驗證別人的程式好麻煩,尤其是不知道作者的意圖的時候。不過話說回來,或許我會用 Frama-C 驗證我自己的程式。
  • Denotation Semantics 指稱語意 - 這堂課和 Operational Semantics 一樣,是在探討程式的語意。然而不同的是 Denotational Semantics 比較在乎函數的 Composition,所有的 Statement 都被理解成更簡單的 Statement 的 Composition。在這個課程當中,我學到了所謂 Recursion 其實是尋找更高一層函式的 Fix Point,這對我來說是一個很特別的想法!過去我想到 Recursion 只會想到 Stack 與 Calling convention 之類的東西。
總體來說,FLOLAC 2010 非常精彩,而且學到了很多不同的東西!雖然有人開玩笑地說 FLOLAC 2010 是夏日裡的煉獄,不過我個人的感覺是:在考試之前,都只是適量,不過考試嗎... 別再提了。

但是總結來說,FLOLAC 2010 兩個星期的課程值回票價,我很喜歡!