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 虛擬裝置相互溝通。

參考資料