2012年11月28日 星期三

在 x86-64 上對 system call 使用 conditional break

在了解目標模組的動態行為時, 最近覺得從模組的 public interface 設中斷點是滿不錯的作法。廣意來說, system call 則是最一般化的 public interface, 從 system call 回頭夾擊目標也滿有效的。比方像這篇strace 找到切入點。

若想獲得更多資訊, 可以用 gdb 在 system call 上設中斷點, 一步步從退回來找到呼叫者、傳遞的資料等更多訊息。但在尋找 write、send 這類廣為使用的 system call 時, 被呼叫的次數過於頻繁, 就有點難找了。

查了一下, 看到這篇提到從 register 設定 conditional break 的方法:

(gdb) b write if 1==$rdi  # stop only when write(1, ...)

Scott 補充了以下的資訊:

要在 syscall argument 上設條件,且要 portable 以目前的工具不容易。用 gdb 的話需能背出『x86-64 上參數依序擺在 rdi, rsi, rdx, rcx, r8d, r9d』。 x86-64 上特別好記,因 syscall 與 一般 function call 被設計成將參數擺在同位置。ARM 上還有 64bit 參數開頭一定擺在偶數暫存器如 r0-r1, r2-r3 而不擺在 r1-r2 等規則。

目前 gdb 沒有將 syscall parameter 存在如 $syscall_arg0 的 convenience variable 中,否則寫: catch syscall write if $syscall_arg0 == 1 即可。

用 "system call calling convention" 當關鍵字查到《What are the calling conventions for UNIX & Linux system calls on x86-64》, 文中有許多相關資訊, 就先備忘吧。

2012年11月24日 星期六

善用 prctl(PR_SET_NAME, name) 協助 debug multi-thread

參考文章: Name your threads

man 2 prtcl 說 prctl(PR_SET_NAME, char*) 會改變 process name, 但是它其實改的是 thread name。使用 gdb 的 info thread 會顯示 thread name, 這對於除錯 multi-thread 很有幫助。需要注意的是, 小心別改到 main thread 的名稱, 避免其它相關 kill 指令或自己寫的 script 失效。

2012-11-25 更新

Scott 提醒, 使用 pthread_setname_np(pthread_t, const char*) 更頗當, 且可用來設別的 thread 的名字。注意參數是 pthread_t 不是 tid, 還有不知為何, Ubuntu 12.04 下沒有它的 man page, 但 /usr/include/pthread.h 有它的宣告, 且編譯連結試用後, 也沒有問題。

2012年11月22日 星期四

善用 shared library visibility 減少程式之間的衝突

關於 static library 和 shared library 的基本知識:

static library 沒有特別的, 但是 shared library 有不少神奇的功能可用。

想像一個情境, 有四個獨立的專案 A, B, X, Y 四者, 其中 A 用到 B, X 用到 Y。若 A 也想用到 X, 但 B 和 Y 有重覆的 symbol, 得在編譯和連結時動點手腳, 才能過關。

上面情境的示意圖:

A -> B
|
v
X -> Y

假設編譯 B, X, Y 時產生 static library, 在產生執行檔 A 時, ld 會抱怨某些 symbol 衝突 (在 B 和 Y 裡面)。

static library 只是一堆 object file 的集合體, 就像 tar 包覆一堆檔案一般, 只是 static library 裝的是 binary 並且可以加入 index 檔。也因此 static library 通常比 shared library 大包, 因為 static library 不管 (也無法知道) 每個 symbol 最後是否會被用到, 總之就先留著它。

shared library 多了不少功能可用, 其中一個實用的功能是 visibility。可透過編譯 (非連結) 時下參數隱藏不需要的 symbol。以上面的例子來說, 若 A 只用到 X 且不會用到 Y, 那麼, 改用 shared library 的 visibility, 可以避開 B 和 Y 衝突的 symbol, 作法如下所述。

為簡化描述, 以下用單一檔案表示一個專案:

1. 產生 libXY.so

$ g++ -c Y.cpp -fPIC -fvisibility=hidden
$ g++ -c X.cpp -fPIC
$ g++ -shared -o libXY.so X.o Y.o

注意: 編譯 Y.cpp 時多了 -fvisibility=hidden, 表示除非程式內有用 g++ 特有的語法指定 visibility (__attribute__((__visibility__("default"))), 不然預設行為變成:

  • 無論原本 C++ 語意的 scope 為何, shared library 外看不見此 object file 的 symbol
  • shared library 內仍採用 C++ 的語意

對部份平台來說, -fPIC 是編譯 shared library 的必要條件, 先當作編譯 shared library 時需要在編譯時加上 -fPIC 吧。

2. 產生 libB.a

$ g++ -c B.cpp
$ ar rvs libB.a B.o

shared 或 static library 在此無關緊要

3. 產生執行檔 A

$ g++ -c A.cpp
$ g++ A.o libB.a libXY.so -o A

備註:

2011-11-24 更新

Scott 補充有關 PIC 的說明, 我就直接備忘在這裡啦。

相關 key word: "text relocation" [1] "text" 在此指機械碼,跟可執行檔中擺機械碼的 section 稱為 .text section 一樣。"relocation" 指『有參考到某 symbol,故連結器需一併修改的地方』。有加 *-fPIC*,compiler 產出的機械碼就不會有 text relocation,dynamic linker 才願意在執行時載入。

在幾乎全部平台上, -fPIC 或 -fpic 都是必要的。10 年前 i386 Linux 上不加也可以,但現在連 i386 預設也會被 security policy 拒絕。*elfutils* 中有個 eu-findtexrel 可找出 shard library 中沒加 "-fPIC" 編譯的 .o。

運作原理上,若沒加 -fPIC ,compiler 產出的機械碼中每個用到全域變數與函式的地方都會嵌有該 symbol 的位址。即 shared library .text section 中會散佈很多 memory reference。但那些位址在執行期間需被 dynamic linker 修改。加了 -fPIC ,產生出來的機械碼 reference 全域變數與函式的方法就不一樣了 [2]。

在安全性上不希望一頁記憶體既可寫又可執行;從多 process 共用 shared library .text section 的 physical memory pages 角度來看,希望 .text section 保持不變且 read only。 所以後來 dynamic linker 遇到有 text relocation 的 shared library 就拒絕載入了。

[1]: http://www.akkadia.org/drepper/textrelocs.html [2]: http://www.iecc.com/linker/linker10.html

GNU Makefile 雜項語法備忘

一般的 tutorial 教得都差不多卻少了一些我想知道的語法, 以下是自己備忘用的語法, 對於讀別人的 Makefile 時有幫助

$ cat Makefile
var ?= xxx         # assign var = xxx if var is not assigned
.PHONY: all        # tell make that "all" is not a file
all:               # first target is the default target
    @echo make-all # @ means do not display the cmd
    @echo all: x $(x)

all: b             # all depends on b

b:
    @echo make-b   # must use TAB to indent actions
    @echo b: var $(var)


all: a             # now all depends on a and b

a: x:= 3           # set x = 3 only in this context
a:
    @echo make-a
    @echo a: x $(x)

reverse = $(2) $(1)  # define a function
c:
    @echo c: x y
    @echo c: $(call reverse, x, y)  # use "call" to use
                                    # defined functions

範例輸出

$ make
make-a
a: x 3
make-b
b: var xxx
make-all
all: x
$ var=ooo make b  # override var
make-b
b: var ooo
$ x=9 make  # set x = 9 globally
make-a
a: x 3   # note that x is still 3
make-b
b: var xxx
make-all
all: x 9
$ make c
c: x y
c: y x

注意 Makefile 自己有套變數, 想改變 g++ 的參數時, Makefile 必須寫成傳遞變數作為 g++ 參數, 比方說慣例上會用 CFLAGS, CXXFLAGS, LDFLAGS 之類的。若 Makefile 沒這麼定, 想改變 g++ 用的參數時, 就只能直接修改 Makefile 了。

2012-11-25 更新

Scott 提醒, 補上有用的參考資料:

2014-01-20 更新

更新連結, 還有補上 Scott 在留言裡補充的說明:

用『=』定義的變數像是建立一個數學關係式,每次用到該變數時會重新解譯、計算出新的值。

2012年11月18日 星期日

查詢 Ubuntu package 編譯時的參數

GDB Python API 提到要在編譯 gdb 時有加 --with-python 才會支援此功能, 想確認編譯 Ubuntu 12.04 的 gdb 時, 是否有下參數 --with-python, 參考資料: compiling - Where can I find the configure options used to build a package? - Ask Ubuntu

摘要回覆的作法如下

抓原始碼看編譯規則

$ apt-get source gdb
$ cd gdb-7.4-2012.04/
$ vi debian/rules

然後看到兩組 configure rule, 一個對到 --without-python, 另一個對到 --with-python, 嗯..., 雖然看起來比較像後者, 還是有些懷疑

查官方 build log

$ apt-cache showpkg gdb

得知名稱是 7.4-2012.04-0ubuntu2, 接著到 https://launchpad.net/ubuntu/+source/gdb 查詢:

  • click 7.4-2012.04-0ubuntu2 updates (main) 2012-05-15
  • click Builds 下的 amd64
  • click Build status 下的 buildlog

查詢 ./configure 找到 --with-python, 所以應該是有加上 --with-python 才是

2012年11月17日 星期六

以使用 libsqlite 為例說明如何找到程式的進入點

一直對於如何善用 runtime 資訊找到程式進入點很有興趣, 終於有個不錯的小例子。

目標

假設要觀察的專案有用到 sqlite 儲存資料, 從使用方式知道一開始會先載入 sqlite 的內容, 現在想找到程式讀取第一筆資料的進入點。

第一步先觀察程式如何使用 sqlite, 是用 shared lib 或是直接包含在程式裡:

$ ldd PROG | grep sqlite

Case 1: 沒有結果, 表示 sqlite 的實作直接含在 PROG 裡面

找出和 sqlite 相關的 API:

$ nm PROG | grep sqlite | awk '{print $NF}' | xargs c++filt

或是查官網文件也成, 不過個人覺得 nm + c++filt 這招比較方便也比較酷。

觀察一下後, 得知開檔的 API 有: sqlite3_open, sqlite3_open16, sqlite3_open_v2, 之後用 cgdb 執行程式, 都設中斷點, 就結案了。

Case 2: 有 grep 結果, 表示在 shared lib 裡

若不想看官方文件, 也想來個動態搜集 API, 可用 ltrace:

$ ltrace -l /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 PROG

其中 libsqlite3 的路徑是從 ldd PROG 得知的。

安裝需要的 debug symbol, 以做進一步觀察:

$ aptitude search sqlite | grep dbg # 找到 libsqlite3-0-dbg
$ sudo aptitude install libsqlite3-0-dbg

再來就用 cgdb 執行程式, 準備設中斷點, 結案。

相關參考資料:

以 abstract class 為例說明 C++ 編譯器和連結器的運作

ps. 經 Scott 提醒發覺內容有誤, 已更新內文。

Effective C++ item 7 提到, 若想使用 abstract class A, 建議這麼做:

// a.h
class A {
public:
  virtual ~A() = 0; // 注意只有宣告沒有實作, 是 pure virtual
};

但是還是得另外提供 ~A() 的實作:

// a.cpp
#include "a.h"
A::~A() {}

不然連結時會出錯

以 class B 繼承 A 為例, 假設程式如下:

// b.h
#include "a.h"
class B : public A {
};

// b.cpp
#include "b.h"
// Nothing.

先來看少了 a.cpp 的情況。由於編譯時只會看 header, 編譯器會阻止任何嘗試生成 A 的程式, 這是我們想要的好結果, 利用宣告含有至少一個 pure virtual 函式的作法, 得到 abstract class 的性質。用 destructor 是不錯的選擇, 不會因此增加不必要的 virtual method, 況且 「base class + 使用多型」的情況要有 virtual destructor, 和使用 abstract class 的目的相合。

然後, 來看編譯 b.cpp 得到的 b.o。不論是否有呼叫到 B() 或 ~B(), 編譯器都會產生 B() 和 ~B() 的 binary code。編譯器對照 a.h 和 b.h 覺得 access level 沒有問題, 不會有編譯錯誤, 其中 ~B() 會呼叫 ~A(), ~A() 的 symbol 還未定義, 等待連結時補上。

但是, 若少了在 a.cpp 內 ~A() 的實作的話, 連結時會發覺找不到 ~A() 的 symbol, 於是有 link error。就語法來說這結果挺怪異的, 宣告為 pure virtual 卻又得提供實作才行。反之, 從實作面來看 compiler 和 linker 怎麼運作, 就不會覺得奇怪。又一次讓我覺得要理解 C++ 的語法, 得從運作的方式來理解才行。只看語法的話, 不太容易理解和記憶。

關聯文章:

2012年11月15日 星期四

編譯或連結錯誤的檢錯流程 (初版)

開發環境是 Ubuntu。先寫篇草稿, 日後慢慢補完。

找不到 xxx.h

檢查是否有 xxx.h

$ sudo updatedb && locate xxx.h

若 OS 內沒有的話, 看看要裝什麼套件才有

$ sudo apt-file update && apt-file search xxx.h

確定有檔案後, 檢查使用 libxxx 需要用的編譯參數為何

$ pkg-config --cflags xxx

若不確定 pkg-config 參數的名稱, 使用 apt-file search 查到的 package 名稱 "PKG-X", 查詢 PKG-X 包含的檔案

$ dpkg -L PKG-X | grep pkgconfig

比對編譯時用的參數, 是否有含到正確的 include path (參數 -I), 沒有的話, 可能是 makefile 出錯, 檢查產生 makefile 的設定檔是否正確

若 OS 內沒有 xxx.h, 也沒有任何一個套件含有 xxx.h, 可能目前 OS 太舊, 用 Ubuntu Packages Search 查詢 xxx.h, 確認是否新版的 OS 才有 xxx.h

相關文章

2012年11月3日 星期六

C++ 隱藏共用的 helper class

以前寫 Java 時滿習慣用 helper class 分擔主 class 的一些工作, 簡化主 class 本體的複雜度, 或是提供輔助用的 data object, 但只在內部使用, 不讓外部使用。在 Java 裡滿直覺的, helper class 宣告成 package-private 即可。但在 C++ 的情況, 明白 compiler 怎麼編譯 C++ 程式後, 才想通作法。

程式如下:

a.h

class X;

class A {
public:
  A();
  void print();

private:
  X* x;
};

class B {
public:
  B();
  void print();

private:
  X* x;
};

a.cpp

#include <iostream>
#include "a.h"

class X {
public:
  void print(const char *s) {
    std::cout << s << std::endl;
  }
};

A::A() : x(new X()) {}

B::B() : x(new X()) {}

void A::print() { x->print("A"); }

void B::print() { x->print("B"); }

int main(void) {
  A a;
  B b;
  a.print();
  b.print();
  return 0;
}

兩個關鍵:

  • a.h 需要知道 X 是 class, 不是天外飛來的不知名符號, 必須有 forward declaration class X。include a.h 的程式沒有 X 的完整宣告, 自然也無法使用 X
  • class A 和 B 不能宣告 member field 為 X x, 必須用指標, 因為在宣告 A 或 B 的物件時, compiler 要知道該在 stack 上配置多少空間, 但 a.h 裡沒提到, 所以除了 a.cpp 以外引入 a.h 的程式也不會知道, 這和 Pimpl 宣告實作物件為指標是同樣的原因。用指標的話則無此問題, 指標不論型別大小都一樣

相較於其它語言, C++ 給我的感覺是, 從思考 Compiler 如何生成程式的角度來看, 會比較容易理解它的語法和限制

C++ 能否用 memcpy 複製 class / struct 的資料?

答案是: POD (plain old data) type 可以。POD type 可和 C 互通, CPP Reference POD Type 的介紹: Specifies that the type is POD (Plain Old Data) type. Thi...