My code works, I don’t know why.

國王的耳朵是驢耳朵

使用dpkg 查詢套件資訊

| Comments

dpkg全名是Debian package,望文生義可以知道就是Debian套件管理程式。使用過Ubuntu/Debian 應該看過deb檔案。這邊列出幾個常用的功能。測試環境是Ubuntu 12.04.4

  • 顯示套件狀態:
    • dpkg -s 套件名稱
  • 顯示套件安裝到系統的檔案分佈
    • dpkg -L 套件名稱
  • 列出套件說明
    • dpkg -l 套件名稱
  • 搜尋檔案屬於那些套件
    • dpkg -S 搜尋的檔案如stdio.h

同場加碼:apt-file這個套件也可以查詢檔案屬於哪個套件。

Debian打包準備套件和文件

| Comments

Debian New Maintainers’ Guide順便整理一下Debain官方文件建議打包套件可以安裝的套件文件

套件

  • build-essential
  • autoconf
  • automake
  • autotools-dev
  • debhelper
    • 協助產生套件skeleton
  • dh-make
    • 協助產生套件skeleton
  • devscripts
  • fakeroot
    • 模擬root權限。
  • file
  • git
  • gnupg
    • 簽核用
  • lintian
    • 協助檢查Debian套件打包錯誤
  • patch
  • patchutils
  • pbuilder
    • Debian 套件用的personal package builder
  • perl
  • python
  • quilt
    • 協助管理大量的patch file
  • xutils-dev
    • 通常是X11會用到的工具如抽出巨集。
  • gpc
    • Pascal編譯器,依專案需要(Ubuntu 已經不maintain)
  • gfortran
    • Fortran編譯器,依專案需要

懶人包如下:(不包含gpc)

1
sudo apt-get install build-essential autoconf automake autotools-dev debhelper dh-make devscripts fakeroot file git gnupg lintian patch patchutils pbuilder perl python quilt xutils-dev gfortran

文件

其他資源

參考資料:

GNU Build System: Autotools 初探

| Comments

身為組裝工,常常執行下面的指令

  • ./configure
  • make
  • 組裝

用了那麼久從來沒有思考過這是什麼樣的東西,慚愧之餘趕快惡補一下。


目錄


Overview

緣由:

雖然C語言號稱高移植性,然而遺憾的是因為System call, library等介面早期並沒有規範。這造成使用C語言開發多平台軟體的時候需要針對不同的平台做許多的檢查。後來GNU針對這部份提出了autotool工具減輕開發的負擔。

如果懶得看元件簡介,請直接看Overoview結論


Autotool 元件

從Wikipedia可以看到GNU Build system的項目中有提到三個元件: * Autoconf * Automake * Libtool


Autoconf - GNU Project - Free Software Foundation (FSF)

這邊可以看到configure負責產生

  • Makefile
    • 讓你編譯程式
  • C option用的header file (optional)
    • config.h
  • configure.status
    • 重新自動產生上面的資料
  • configure.cache (optional)
    • cache之前configure偵測的系統結果

套件中有幾個程式

  • autoscan
    • 產生configure.ac
  • autoconf
    • configure.ac中產生configure
    • 為何是呢?這和autoconf的版本有關
  • autoheader
    • configure.ac產生config.h.in
  • ifnames
    • 掃描C原始碼,抽出ifdef的資訊

Automake - GNU Project - Free Software Foundation (FSF)

套件中有兩個程式

  • automake
    • Makefile.am中產生Makefile.in,讓configure執行時可以產生Makefile
  • aclocal
    • configure.acconfigure.in產生aclocal.m4

GNU Libtool - The GNU Portable Library Tool

  • 提供抽象化的介面處理函式庫的平台差異問題,本篇暫不討論。

另外GNU 其他針對移植性輔助的套件有

  • GNU gettext
    • i18n套件,協助專案中的各國語言訊息開發。
  • pkg-config
    • 提供開發時的函式庫資訊,開發者不需要知道函式庫的路徑和旗標,直接問pkg-config就好。
  • GCC
    • 全名是GNU Compiler Collection

合體

從Wikipedia的圖:Autoconf-automake-process解釋的非常清楚。

回到最前面,我們可以觀察到autotool的目的就是

  • 產生平台上可以編譯的環境

為了達到這樣的目的,系統需要做到下面的功能

  • 檢查平台環境
  • 產生Makefile

因此,開發者最少要告訴autotools 所需平台環境產生Makefile 這兩項資訊。這也是configure.acMakefile.am存在的目的。也就是說,開發者需要自行設定configure.acMakefile.am

而這些錯綜複雜的關係可以描述如下

  • autotool先產生configure.ac (透過autoscan或是自幹)
  • autoreconfconfigure.acMakefile.am產生confgure, config.h.in, 和Makefile.in
  • autoreconf細節
    • configure.ac,呼叫aclocal產生aclocal.m4
    • aclocal是給autotools中自訂專案檢查編譯巨集用。
      • configure.acaclocal.m4,呼叫autoheader產生config.h.in
      • configure.acaclocal.m4,呼叫autoconf產生configure
      • configure.acaclocal.m4和各處的Makefile.am,呼叫automake產生Makefile.in

範例


測試環境

  • Ubuntu 12.04.4

測試程式

範例程式細節在這邊,檔案各別重新分配到src, include, libs這三個目錄。不想看code只要知道每個檔案都有參考到某個自訂的header file就好了。

測試程式樹狀架構
1
2
3
4
5
6
7
8
├── include
│   ├── liba.h
│   └── libb.h
├── libs
│   ├── liba.c
│   └── libb.c
└── src
    └── test.c

導入Autotools

簡述一下流程

  • 使用autoscan產生configure.ac範本
  • 手動更改configure.ac
  • Makefile.am,根目錄以及需要編譯的地方都要一份
  • autoreconf --install產生檔案
    • 替代方案:執行下面程式
      • aclocal
      • autoheader
      • automake --add-missing
      • autoconf
  • configure
  • make
  • make install安裝或make dist打包

configure.ac

執行autoscan後會產生configure.scan檔案,把這個檔案rename成configure.ac

configure.ac最初範本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ([2.68])
AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
AC_CONFIG_SRCDIR([libs/libb.c])
AC_CONFIG_HEADERS([config.h])

# Checks for programs.
AC_PROG_CC

# Checks for libraries.

# Checks for header files.
AC_CHECK_HEADERS([stdlib.h])

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.
AC_FUNC_MALLOC

AC_CONFIG_FILES([Makefile
                 libs/Makefile
                 src/Makefile])
AC_OUTPUT

接下來就是更動configure.ac,更動完和原本的差別如下:

configure.scan和configure.ac 差別
1
2
3
4
5
6
7
8
9
10
5,6c5,6
< AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
< AC_CONFIG_SRCDIR([libs/libb.c])
---
> AC_INIT([Test_Autotools], [0], [test])
> AM_INIT_AUTOMAKE([foreign -Wall -Werror])
8a9,14
> # Use static libary
> AC_PROG_RANLIB
>

簡單說明如下

  • 更改套件版本資訊
  • 刪除AC_CONFIG_SRCDIR
  • 設定automake
    • foreign表示這不是標準的GNU Coding Standard,因此不會檢查額外的檔案如NEWS, README, ChangeLog等。
    • 共用編譯參數
  • AC_PROG_RANLIB表示要使用static library

Makefile.am

前面提到,每個目錄都要有一個Makefile.am。關於Makefile.am語法,可以參考 Alexandre Duret Lutz: Autotools Tutorial大略了解。更詳細的部份可以看Automake手冊:8.3 Building a Shared Library

範例中Makefile.am如下:

  • ./Makefile.am: 基本上就是依照列出的順序編譯
./Makefile.am
1
SUBDIRS = libs src
  • libs/Makefile.am
libs/Makefile.am
1
2
3
4
5
6
AM_CFLAGS = -I../include
lib_LIBRARIES = liba.a libb.a
liba_a_SOURCES = liba.c
libb_a_SOURCES = libb.c

include_HEADERS = ../include/liba.h ../include/libb.h

最上面形式就是要裝到目錄_Keyword,所以表示我們要產生liba.alibb.a,安裝時放在/usr/local/lib下面、指定library由哪些原始程式檔案組成、同時要把header files放在/usr/local/include下。(configure沒指定預設prefix為/usr/local)

  • src/Makefile.am
src/Makefile.am
1
2
3
4
5
LDADD = ../libs/liba.a ../libs/libb.a
AM_CFLAGS = -I../include

bin_PROGRAMS = test
test_SOURCES = test.c

一樣的形式:要裝到目錄_Keyword。這邊宣告要安裝到/usr/loca/bin、要link liba.alibb.a、並且5指定執行檔的原始程式檔案。


經過上面的處理,我們多了三個Makefile.amconfigure.ac以及一些暫存檔案。新的樹狀目錄列表如下:

autoscan後測試程式樹狀架構
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── autom4te.cache
│   ├── output.0
│   ├── requests
│   └── traces.0
├── autoscan.log
├── configure.ac
├── include
│   ├── liba.h
│   └── libb.h
├── libs
│   ├── liba.c
│   ├── libb.c
│   └── Makefile.am
├── Makefile.am
└── src
    ├── Makefile.am
    └── test.c

產生configure並編譯

如前所述,直接使用autoreconf --install就可以了

autoreconf –install以及之後的測試程式樹狀架構
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ autoreconf --install
configure.ac:6: installing `./install-sh'
configure.ac:6: installing `./missing'
libs/Makefile.am: installing `./depcomp'

.
├── aclocal.m4
├── autom4te.cache
│   ├── output.0
│   ├── output.1
│   ├── output.2
│   ├── requests
│   ├── traces.0
│   ├── traces.1
│   └── traces.2
├── autoscan.log
├── config.h.in
├── configure
├── configure.ac
├── depcomp
├── include
│   ├── liba.h
│   └── libb.h
├── install-sh
├── libs
│   ├── liba.c
│   ├── libb.c
│   ├── Makefile.am
│   └── Makefile.in
├── Makefile.am
├── Makefile.in
├── missing
└── src
    ├── Makefile.am
    ├── Makefile.in
    └── test.c

./configure結果節錄如下

autoreconf –install以及之後的測試程式樹狀架構
1
2
3
4
5
6
7
8
9
10
11
$ ./configure --prefix=/tmp/build
checking for a BSD-compatible install... /usr/bin/install -c
...
checking for GNU libc compatible malloc... yes
configure: creating ./config.status
config.status: creating Makefile
config.status: creating src/Makefile
config.status: creating libs/Makefile
config.status: creating config.h
config.status: config.h is unchanged
config.status: executing depfiles commands

幾點要注意的:

  • config.status被產生,並且被執行時生出config.h以及各目錄的的Makefile
  • 這邊指定prefix為/tmp/build,因此make install可以看到被安裝目錄樹狀結構如下:
安裝目錄樹狀結構
1
2
3
4
5
6
7
8
9
.
├── bin
│   └── test
├── include
│   ├── liba.h
│   └── libb.h
└── lib
    ├── liba.a
    └── libb.a

參考資料及資源

強烈推荐 Alexandre Duret Lutz: Autotools Tutorial,主要的想法和資料都是從這邊出來。而且他寫的更加詳細、更加易懂。

C99的inline Function

| Comments

inline function是一個keyword,提醒compiler可以將function本體直接填入呼叫該function的位置。從這邊可以看到,優點為

  • 減少呼叫function的overhead,如參數傳遞、回傳值處理等。
  • 切換到function會jump,也就是會有branch的行為。因為CPU的pipeline和cache的特性,會影響效能。
  • 經由代換程式碼,也許可以增加最佳化的可能性。

廢話少說,先來一段程式碼。 測試環境

  • Ubuntu 12.04.4
  • GCC 4.6.3
inline_test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

inline void hello()
{
    printf("Hello World\n");
}

int main(void)
{
    hello();

    return 0;
}

可以正常編譯

build.log
1
2
$ cc -g -Wall -Werror -c -o inline_test.o inline_test.c
$ cc -g -Wall -Werror -o inline_test inline_test.o

有趣的是,換成C99就會編譯錯誤,檢查symbol的確不存在。

build.log
1
2
3
4
5
6
7
8
9
10
cc -g -Wall -Werror -std=c99   -c -o inline_test.o inline_test.c
cc -g -Wall -Werror -std=c99 -o inline_test inline_test.o
inline_test.o: In function `main':
inline_test.c:10: undefined reference to `hello'
collect2: ld returned 1 exit status
make: *** [inline_test] Error 1

$ nm inline_test.o
                 U hello
0000000000000000 T main

這邊可以看到C99的inline定義是和GNU C的extern inline相反。所以最簡單的懶人法就是在C99裏面加上extern收工。

inline_test_c99.c
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

extern inline void hello()
{
    printf("Hello World\n");
}

int main(void)
{
    hello();

    return 0;
}

加碼測試

不過對於組裝工而言,為何要在inline前面加extern或是static實在有趣,所以多測了幾下。沒興趣的就直接看結論

GNU C測試

如果我們把程式碼改成

test_inline.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

extern inline void hello()
{
    int i = 100;
    printf("Hello World: %d\n", i);
}

inline void hello()
{
    printf("Hello World 2\n");
}

int main(void)
{
    hello();

    return 0;
}

在GNU C下編譯執行會印Hello World 2,使用objdump -S反組譯可以看到

  • main 呼叫4004f4位址
  • 4004f4真正的程式碼是印出Hello World2,也就是說extern inline void hello()裏面的程式碼是寫心酸的。
  • 看起來這種情況GCC沒有把inline function展開。

另外一點有趣的是C99下面把extern inlineinline對調會編譯失敗。

inline_test.lst
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
00000000004004f4 <hello>:
    int i = 100;
    printf("Hello World: %d\n", i);
}

inline void hello()
{
  4004f4: 55                      push   %rbp
  4004f5: 48 89 e5                mov    %rsp,%rbp
    printf("Hello World 2\n");
  4004f8: bf 0c 06 40 00          mov    $0x40060c,%edi
  4004fd: e8 ee fe ff ff          callq  4003f0 <puts@plt>
}
  400502: 5d                      pop    %rbp
  400503: c3                      retq

0000000000400504 <main>:

int main(void)
{
  400504: 55                      push   %rbp
  400505: 48 89 e5                mov    %rsp,%rbp
    hello();
  400508: b8 00 00 00 00          mov    $0x0,%eax
  40050d: e8 e2 ff ff ff          callq  4004f4 <hello>

    return 0;
  400512: b8 00 00 00 00          mov    $0x0,%eax
}

結論

  • GNU C和C99 inline的extern定義相反。
  • C99下面只有inline只是一個宣告,不會產生symbol。要使用extern編譯器才會產生symbol。猜測可能單純inline是在header file宣告用,而extern inline則是在source code實作時使用。

參考資料

Linux中使用C語言載入data Object 檔案資料 (續)

| Comments

動機

前面討論了Linux中使用C語言載入data object 檔案資料,裏面提到資料轉成object,裏面會有三個symbol: _binary_objfile_start, _binary_objfile_end, _binary_objfile_size

Object symbol

man objcopy_size可以看到

1
2
3
4
5
6
   -B bfdarch
   --binary-architecture=bfdarch
       Useful when transforming a architecture-less input file into an object file.  In this case the output architecture can be set to bfdarch.  This
       option will be ignored if the input file has a known bfdarch.  You can access this binary data inside a program by referencing the special
       symbols that are created by the conversion process.  These symbols are called _binary_objfile_start, _binary_objfile_end and
       _binary_objfile_size.  e.g. you can transform a picture file into an object file and then access it in your code using these symbols.

測試: Segmentation fault版

簡單來說,和平台無關的檔案轉成obj檔時,可以透過_binary_objfile_start, _binary_objfile_end, _binary_objfile_size去存取資料。所以我再修改了一下測試程式,印出這些symbol 的內容

test_obj.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <string.h>

#define MAX_CHARS (32)

extern char _binary_my_data_txt_start;
extern char _binary_my_data_txt_end;
extern int _binary_my_data_txt_size;

int main(int argc, char **argv)
{
    char line[MAX_CHARS] = {0};
    char *my_data_start_addr = (char *) &_binary_my_data_txt_start;
    int str_len = (char) *my_data_start_addr - 0x30; /* 0x30 => '0' */

    if (str_len > MAX_CHARS) {
        str_len = MAX_CHARS;
    }

    strncpy(line, my_data_start_addr + 1, str_len);

    printf("_binary_my_data_txt_start: %c\n", _binary_my_data_txt_start);
    printf("_binary_my_data_txt_end: %c\n", _binary_my_data_txt_end);
    printf("_binary_my_data_txt_size: %d\n", _binary_my_data_txt_size);
    printf("Read string is %s\n", line);

    return 0;
}

而測試檔案為

my_data.txt
1
5Hello

一跑起來會發生Segmentation fault如下

my_data.txt
1
2
3
4
$ ./test_obj
_binary_my_data_txt_start: 5
_binary_my_data_txt_end:
Segmentation fault (core dumped)

分析

使用gdb可以看到當在存取_binary_my_data_txt_size這行,我們還可以進一步來看這個變數

gdb 結果
1
2
3
4
5
6
7
8
_binary_my_data_txt_start: 5
_binary_my_data_txt_end:

Program received signal SIGSEGV, Segmentation fault.
main (argc=1, argv=0x7fffffffe1d8) at test_obj.c:24
24        printf("_binary_my_data_txt_size: %d\n", _binary_my_data_txt_size);
(gdb) p _binary_my_data_txt_size
Cannot access memory at address 0x7

疑?變數位址是0x7?那我把測試檔案內容改一下,增加3個字元,再跑一次gdb。

my_data.txt
1
8HelloABC
gdb 結果
1
2
(gdb) p _binary_my_data_txt_size
Cannot access memory at address 0xa

可以看到位址從0x7跑到0xa,也就是10,這表示這個數字增加三了。這讓我懷疑這個symbol並不是一個變數,而是一個數值。因此我們可以將 * printf("_binary_my_data_txt_size: %d\n", _binary_my_data_txt_size);

改成
  • printf("_binary_my_data_txt_size: %p\n", &_binary_my_data_txt_size);

另外一個有趣的地方是_binary_my_data_txt_end並不是C,而是\0。而_binary_my_data_txt_end前一個字元是\n。使用ghex去看my_data.txt可以看到最後一個資料其實是\n。但是\0怎麼出現的,目前還不清楚。

參考資料

Linux中使用C語言載入data Object 檔案資料

| Comments

之前看別人的程式,看到作者在ROMFS實作時把資料轉成object檔案,然後在C語言中直接存取資料。他的作法是

  • 寫一個host程式,將某個目錄下面的檔案和目錄轉成單一個binary
  • 將這個binary轉成object檔案
  • 更改link script,讓最後的binary可以存取object檔案的section

這個project使ARM的架構Realtime 作業系統。看完手癢也想看看Linux下面要怎麼做到類似的功能。本來想說是不是要動到link script,後來看到這邊的參考資料後,發現只要存取symbol就可以使用了。克服這個問題後就可以來寫個小程式驗證一下。

首先我們先弄個測試資料,很簡單就是一個數字顯示後面字串長度後再存放字串。

my_data.txt
1
5Hello

接下來就是Makefile部份

Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CFLAGS=-Wall -Werror -g

DATA_SRC=my_data.txt
DATA_OBJ=my_data.o

TARGET=test_obj
OBJS=$(patsubst %, %.o, $(TARGET)) $(DATA_OBJ)

BFN=elf64-x86-64
ARCH=i386

$(TARGET): $(OBJS)
  $(CC) -o $@ $(OBJS)

$(DATA_OBJ): $(DATA_SRC)
  objcopy -I binary -O $(BFN) -B $(ARCH) --prefix-sections '.mydata' $^ $@

clean:
  rm *.o *~ $(TARGET) -f

Makefile說明: * patsubst * Makefile 提供的函數,用在pattern 替換,所以上面做了下列的代換 * test_obj轉成test_obj.o * $@ * Makefile的內建巨集,代表target * $^ * Makefile的內建巨集,代表prerequisite

重點是把資料檔案轉成object的部份,節錄該部份並說明如下 objcopy -I binary -O $(BFN) -B $(ARCH) --prefix-sections '.mydata' $^ $@

  • -I binary
    • 指定input檔案binary格式為binary
  • -O elf64-x86-64
    • 指定output binary 格式為elf64-x86-64
  • -B i386
    • 指定架構為i386
  • --prefix-sections '.mydata'
    • 指定放在.mydata的section

至於要怎麼知道binary格式和架構呢?您可以用objcopy --info |less配合file test_obj.o找出平台上的參數。

我們先來看一下section是不是真的放到.mydata?

objdump結果
1
2
3
4
5
6
7
8
$ objdump -h my_data.o

my_data.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .mydata.data  00000007  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, DATA

我只知道.mydata的section,裏面的資訊目前還不清楚有什麼意義。

我們再用nm來看檔案內有哪些symbol

nm結果
1
2
3
4
$ nm  my_data.o
0000000000000007 D _binary_my_data_txt_end
0000000000000007 A _binary_my_data_txt_size
0000000000000000 D _binary_my_data_txt_start

flag說明 * D * 資料放在初始的資料section * A * Symbol的值已固定,之後link也不會更動。目前還不知道這樣透露出有什麼額外的資訊

接下來就是存取的方式了,測試程式如下。主要就是從section讀出字串再印出來。

test_obj.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>

#define MAX_CHARS (32)

extern char _binary_my_data_txt_start;
int main(int argc, char **argv)
{
    char line[MAX_CHARS] = {0};
    char *my_data_start_addr = (char *) &_binary_my_data_txt_start;
    int str_len = (char) *my_data_start_addr - 0x30; /* 0x30 => '0' */

    if (str_len > MAX_CHARS) {
        str_len = MAX_CHARS;
    }

    strncpy(line, my_data_start_addr + 1, str_len);

    printf("Read string is %s\n", line);

    return 0;
}

程式說明如下 * 前面nm看到的symbol _binary_my_data_txt_end, _binary_my_data_txt_start, 和_binary_my_data_txt_size只是一個sybmol,這邊我們存的是字元所以宣告成char * _binary_my_data_txt_start存放的是該section開始的資料,我們可以用gdb驗證一下

gdb 節錄
1
2
3
4
Breakpoint 1, main (argc=1, argv=0x7fffffffe1d8) at test_obj.c:10
10        char *my_data_start_addr = (char *) &_binary_my_data_txt_start;
(gdb) p _binary_my_data_txt_start
$1 = 53 '5'
  • 所以我們可以取得該資料位址後,讀出字串長度後,再複製後面的字串並列印出來。

最後執行結果如下

執行結果
1
2
$ ./test_obj
Read string is Hello

參考資料

Hello World

| Comments

C語言學習者第一個程式

hello.c
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char **argv)
{
    printf("Hello world\n");

    return 0;
}

最近有人在問,去掉{ }你真的了解每一行嗎?我試著回答一下

#include <stdio.h>
int main(int argc, char **argv)
printf(“Hello world\n”);
return 0;


  • #include <stdio.h>使用cpp hello.c > hello.i 可以看到
hello.i
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1 "hello.c"
...
extern int printf (__const char *__restrict __format, ...);
extern int sprintf (char *__restrict __s,
      __const char *__restrict __format, ...) __attribute__ ((__nothrow__));

...
# 2 "hello.c" 2

int main(int argc, char **argv)
{
    printf("Hello world\n");

    return 1;
}

直接編譯hello.i並執行

1
2
3
$ gcc hello.i
$ ./a.out
Hello world

* int main(int argc, char **argv)

前面文章可以看到gcc編譯時除了link library和本身的object 檔案外,還會多link一些binary,可以從這邊找看看main在那邊。

說明一下,nm顯示的資料中 U:表示undefined symbol T:表示symbol在Text section中

尋找main
1
2
3
$ nm -A /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/4.6/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/4.6/crtend.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crtn.o 2> /dev/null |grep main
/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o:                 U __libc_start_main
/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o:                 U main

可以看到在crt1.o有一個Undefined symbol叫main,在來看看我們的hello.c這邊

main和hello.c
1
2
3
$ nm hello.o
0000000000000000 T main
                 U puts

所以可以看到crt1.o會用到main(),當然誰去呼叫main這件事我們就視而不見吧。 另外crt1.o可以從man gcc看到被稱為startup file。

  • printf這邊其實要釐清的觀念是,OS透過system call提供服務。因此我們可以用strace去觀察hello呼叫了哪些system call
strace畫面
1
2
3
4
5
6
$ strace ./hello
execve("./hello", ["./hello"], [/* 39 vars */]) = 0
...
write(1, "Hello world\n", 12Hello world
...
exit_group(1)

可以看到事實上printf使用write system call去讓OS印字串到螢幕上。而為什麼不直接用write(1, "Hello world\n", strlen("Hello world\n"));呢?看TLPI(書)有提到主要原因是system call有代價的,而libc實作了buffer減少system call呼叫的次數。有興趣的可以使用man setvbuf看看buffer的設定方式。

  • return 0;可以看下面的程式碼
return_status.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* This demos return status */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    int rval = 0;

    if (argc != 2) {
        printf("usage: %s return_status_number(0~255)\n", argv[0]);
        printf("Then observe return status in $?\n");
        printf("ex: $ %s 121; echo $?\n", argv[0]);

        return 2;
    }

    /* Convert return status to 0~255*/
    rval = atoi(argv[1]);
    rval = rval % 255;

    return rval;
}

跑看看便知道

return_status
1
2
3
4
5
6
7
8
9
10
11
$ ./return_status ; echo $?
usage: ./return_status return_status_number(0~255)
Then observe return status in $?
ex: $ ./return_status 121; echo $?
2

$ ./return_status 219 ; echo $?
219

$ ./return_status 34 ; echo $?
34

$?man bash?可以看到是顯示上次執行回傳狀態。因此不難理解return的值是有人會接起來的。Makefile就使用這個特性判斷build code是否有問題,我們可以測試一下

Makefile
1
2
test:
  ./return_status 147
Makefile執行結果
1
2
3
$ make test
./return_status 147
make: *** [test] Error 147

* gcc -v hello.c輸出

gcc -v hello.c 輸出節錄
1
2
3
4
5
6
7
8
9
$ gcc -v hello.c
Using built-in specs.
COLLECT_GCC=gcc
...
/usr/lib/gcc/x86_64-linux-gnu/4.6/cc1 -quiet -v -imultilib . -imultiarch x86_64-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -fstack-protector -o /tmp/cczq4fLe.s
...
as --64 -o /tmp/ccSFUZMm.o /tmp/cczq4fLe.s
...
/usr/lib/gcc/x86_64-linux-gnu/4.6/collect2 --sysroot=/ --build-id --no-add-needed --as-needed --eh-frame-hdr -m elf_x86_64 --hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/4.6/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/4.6 -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../.. /tmp/ccSFUZMm.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/4.6/crtend.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crtn.o

參考資料:

GNU Ld初探

| Comments

以前一直以為ld單純就是把.a, .o轉成binary,簡單測試一下發現完全不是這樣。

測試的檔案除了Makefile以外,其他的和這邊一樣

將原本的Makefile部份

原本的Makefile準備更動的部份
1
2
$(OUT_DIR)/$(TARGET): $(OUT_OBJS)
    $(CC) -o $@ $^

改成

$(CC)改成ld
1
2
$(OUT_DIR)/$(TARGET): $(OUT_OBJS)
    ld -o $@ $^

一跑make就出現錯誤

make log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ make
mkdir -p build/libs/
cc -g -MMD -I ./include -c libs/liba.c -o build/libs/liba.o
mkdir -p build/libs/
cc -g -MMD -I ./include -c libs/libb.c -o build/libs/libb.o
mkdir -p build/
cc -g -MMD -I ./include -c test.c -o build/test.o
ld -o build/test build/libs/liba.o build/libs/libb.o build/test.o
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0
build/libs/liba.o: In function `from_liba':
./libs/liba.c:11: undefined reference to `puts'
build/libs/libb.o: In function `from_libb':
./libs/libb.c:11: undefined reference to `puts'
build/test.o: In function `main':
./test.c:10: undefined reference to `malloc'
make: *** [build/test] Error 1

ld 這邊再加上-lc又有其他的錯誤,看來的確是有東西隱藏在背面。因此需要有對照組,這時候gcc -v就可以出場了

gcc -v log節錄
1
2
3
4
$ gcc -v -o build/test build/libs/liba.o build/libs/libb.o build/test.o
Using built-in specs.
...
 /usr/lib/gcc/x86_64-linux-gnu/4.6/collect2 --sysroot=/ --build-id --no-add-needed --as-needed --eh-frame-hdr -m elf_x86_64 --hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o build/test /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/4.6/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/4.6 -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../.. build/libs/liba.o build/libs/libb.o build/test.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/4.6/crtend.o /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crtn.o

可以看到gcc呼叫collect2,而collect2會呼叫ld

strace collect2結果節錄
1
2
3
4
5
6
$ strace -e execve -f gcc -o build/test build/libs/liba.o build/libs/libb.o build/test.o
execve("/usr/bin/gcc", ["gcc", "-o", "build/test",...
...
execve("/usr/lib/gcc/x86_64-linux-gnu/4.6/collect2",..., "--sysroot=/",...
...
execve("/usr/bin/ld", ["/usr/bin/ld", "--sysroot=/", ...

最後的Makefile版本變成

ld最後參數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# LD_FLAGS
LD_FLAGS= \
  --sysroot=/ \
  --build-id \
  --no-add-needed --as-needed --eh-frame-hdr \
  -m elf_x86_64 --hash-style=gnu \
  -dynamic-linker /lib64/ld-linux-x86-64.so.2 \
  -z relro /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crt1.o\
  /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crti.o \
  /usr/lib/gcc/x86_64-linux-gnu/4.6/crtbegin.o \
  -L/usr/lib/gcc/x86_64-linux-gnu/4.6 \
  -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu \
  -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../../../lib \
  -L/lib/x86_64-linux-gnu \
  -L/lib/../lib -L/usr/lib/x86_64-linux-gnu \
  -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/4.6/../../..\
  -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s \
  --no-as-needed \
  /usr/lib/gcc/x86_64-linux-gnu/4.6/crtend.o \
  /usr/lib/gcc/x86_64-linux-gnu/4.6/../../../x86_64-linux-gnu/crtn.o

# Build Rules
TARGET=test

$(OUT_DIR)/$(TARGET): $(OUT_OBJS)
  ld -o $@ $^ $(LD_FLAGS)

結論如下

  1. gcc在build code默默處理掉很多細節
  2. gcc -v可以協助顯示更多編譯細節

參考資料:

from Source to Binary: How GNU Toolchain Works

Linker Script初探 - GNU Linker Ld手冊略讀

| Comments

關於一個程式的binary要怎麼存放其實是很有趣的問題,我以前都沒有去想這個問題。後來當組裝工久了以後就忍不住會想知道這些。隨便想一下就有很多問題,例如:

  • 程式碼和資料要怎麼放?
  • 怎麼做到不同的source code共用global 變數?
  • global 變數和local變數放的地方應該不一樣吧?那麼確實不一樣的點是?
  • 呼叫副函數這回事一定是要先找到副函數再跳過去吧?那麼「找到」到底是什麼意思?
  • 如果是用shared library的話,runtime才會找到副函數所在的地方,那麼為什麼編譯的時候不會有錯誤呢? …

這些問題列出來真的是「罄竹難書」,不過我想整體來說至少在Linux下面從binutils下手應該是沒錯。第一個問題應該和linker有關係。所以我先去看ld文件中的linker script,希望可以解決我的疑惑。就算和我的問題無關,至少可以留下一些中文參考資料,造福需要的朋友。

目錄


簡介

ld是GNU linker的程式。ld吃多個object (.o)檔或archive (.a)檔,將他們的資料relocate還有symbol reference資訊一併輸出到新的binary。link通常是compile產生binary的最後步驟。ld在執行的時候依照Linker command language檔案描述去產生binary。ld支援不同的binary format (BFD: Binary File Descriptor)

每次link的時候,都會依照特定的命令去產生新的object檔。而這些命令就是linker script;換句話說,linker script提供一連串的命令讓linker照表操課。Linker script描述的命令有

  • ld吃的object檔案中的section要怎麼map到要輸出的binary檔案。
  • 要輸出的binary檔案要在記憶體中的layout
  • 其他

因為每次link一定會依據linker script去link,所以當ld沒有指定linker script的時候,系統會使用預設的linker script。而ld --verbose可以顯示預設的linkder script。link時指定自幹的linker script則使用ld -T 自己的linker script


背景知識

  • object 檔格式:輸入檔案和輸出檔案所遵循的格式
  • object 檔案:輸入檔案和輸出檔案
  • executable:ld輸出的檔案,有時候會這樣稱呼
  • 每個object檔案都有好幾個section,而
    • input section:輸入object檔案中的section
    • output section:輸出object檔案中的section

Section

  • obj檔案內部有一組section
  • section包含
    • 自己的名稱
    • section contents
    • section長度資訊
    • 狀態
      * loadable: 執行時該section是否需要被載入到記憶體
      * allocatable: 先保留記憶體的一塊空間讓程式執行時使用,如.bss
      
      • section不是loadable 或allocatable 的話一般來說都是給debug用的
  • 參考示意圖: (Jim Huang) How GNU Toolchain Works投影片

Section 記憶體位址

  • Output section如果被載入記憶體,會存放兩種記憶體位址
    • VMA: Virtual Memory Address
    • Runtime 的記憶體位址
    • LMA: Load Memory Address
      • load time的記憶體位址
  • 一般來說,VMA = LMA。不同情況有東西要燒到ROM時參考LMA。從ROM載入到記憶體執行的時候參考VMA
  • 可以使用objdump -h看VMA/LMA資訊

Symbol

  • 一個object 檔案存放多個symbol,又稱為symbol table
  • 將名稱對應到一個記憶體位址的symbol稱為defined symbol,名稱沒有對應到記憶體位址的稱為undefined symbol
  • 名稱通常就是全域變數、靜態變數或是函數的名稱
  • 一般來說,如果把單獨的c編譯成object file時
    • defined symbol為該檔案內的global variable, static varible 和funciton
    • undefined symbol為該檔案內的extern variable和外部funciton
  • 可以使用objdump -t或是nm看symbol資訊

Linker script 格式概論

  • 以文字檔存放
  • 由多個command組成
  • command可能是
    • keyword + 參數
    • 設定symbol
  • command 可以用;分開,空白會被忽略
  • 使用/ .. /註解
  • 字串直接打,如果有用到script保留的字元如.可以用"包住

從Linker script 範例開始

link script 範例
1
2
3
4
5
6
7
8
SECTIONS
{
  . = 0x10000;
  .text : { *(.text) }
  . = 0x8000000;
  .data : { *(.data) }
  .bss : { *(.bss) }
}

這個抄來的範例很簡單,只有一個命令SECTIONSSECTIONS是用來描述執行的時候記憶體的規劃配置(layout)。

說明這個指令細節

  • .表示記憶體位置counter,起始值為0。結束值則由linker 計算把所有input section的資料整合到output section的長度。而.如果沒有指定明確的記憶體位址的話,就會被設定為上一個位址counter的結束位址參考示意圖: (Jim Huang) How GNU Toolchain Works投影片
  • 設定記憶體位置counter為0x10000
  • 接下來請把所有輸入object檔案的程式機械碼中({ *(.text) })存放到輸出object檔案的.text區塊中。
  • 接設定記憶體位置counter為0x8000000
  • 先放有初始值的全域變數(.data)
  • 再放沒有初始值的全域變數(.bss)

另外要注意的是ld會自動幫你處理alignment的問題,所以不用擔心section之間的aligment問題。


linker script 命令格式

  • ENTRY(symbol)
    • 設定某個symbol為程式執行的第一個指令起始點,在我的預設linker script中是ENTRY(_start),然後去反組譯隨便一個C編譯出來的執行檔,找字串_start可以看到裏面又去呼叫了__libc_start_main@plt
hello_word執行檔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Disassembly of section .text:

0000000000400440 <_start>:
  400440:       31 ed                   xor    %ebp,%ebp
  400442:       49 89 d1                mov    %rdx,%r9
  400445:       5e                      pop    %rsi
  400446:       48 89 e2                mov    %rsp,%rdx
  400449:       48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
  40044d:       50                      push   %rax
  40044e:       54                      push   %rsp
  40044f:       49 c7 c0 c0 05 40 00    mov    $0x4005c0,%r8
  400456:       48 c7 c1 50 05 40 00    mov    $0x400550,%rcx
  40045d:       48 c7 c7 2d 05 40 00    mov    $0x40052d,%rdi
  400464:       e8 b7 ff ff ff          callq  400420 <__libc_start_main@plt>
  400469:       f4                      hlt
  40046a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

檔案相關命令

  • INCLUDE filename
    • 在看到這個命令的時候才去載入filename這個linker script。可以被放在不同的命令如SETCTION, MEMORY等。
  • INPUT(file1 file2 ...)
    • 指定載入的輸入object檔案,如abc.o這樣的檔案。
  • GROUP(file1 file2 ...)
    • 指定載入的輸入archieve檔案,如libabc.a這樣的檔案。
  • AS_NEEDED(file1 file2 ...)
    • INPUTGROUP使用的命令,用來告訴linker說如果object裏面的資料有被reference到才link進來,猜測應該可以減少儲存空間。範例(未測試請自行斟酌):INPUT(file1.o file2.o AS_NEEDED(file3.o file4.o))
  • OUTPUT(filename)
    • gcc -o filename 一樣
  • SEARCH_DIR(path)
    • -L path一樣
  • STARTUP(filename)
    • 和INPUT相同,唯一差別是ld保證這個檔案一定是第一個被link

Object檔案相關格式命令

  • OUTPUT_FORMAT(bfdname)
    • 指定輸出object檔案的binary 檔案格式,可以使用objdump -i列出支援的binary 檔案格式
  • OUTPUT_FORMAT(default, big, little)
    • 指定輸出object檔案預設的binary 檔案格式,big endian的binary 檔案格式以及little endian的binary 檔案格式。可以使用objdump -i列出支援的binary 檔案格式
  • TARGET(bfdname)
    • 告訴ld用那種binary 檔案格式讀取輸入object檔案要,可以使用objdump -i列出支援的binary 檔案格式

設定記憶體區塊alias命令

  • REGION_ALIAS(alias, region)
    • 設定MEMORY命令中區塊的alias,一般來說,用在不同的平台需要相同的memory layout時可以使用。舉例來說,當有3個平台,記憶體layout都是相同,那麼可以
      • 將他們平台相關的記憶體區塊MEMORY命令寫在個別的檔案如linkcmds.memory
      • 設定相同的alias
      • 在主要的linker script 使用INCLUDE載入linkcmds.memory,並且直接使用alias當作一般的區塊使用。

詳細的範例說明可以看這邊

範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 INCLUDE linkcmds.memory

 SECTIONS
   {
     .text :
       {
         *(.text)
       } > REGION_TEXT
     .rodata :
       {
         *(.rodata)
         rodata_end = .;
       } > REGION_RODATA
     .data : AT (rodata_end)
       {
         data_start = .;
         *(.data)
       } > REGION_DATA
     data_size = SIZEOF(.data);
     data_load_start = LOADADDR(.data);
     .bss :
       {
         *(.bss)
       } > REGION_BSS
   }

未分類的命令 (節錄)

  • ASSERT(exp, message)
    • 條件不成立就噴訊息並結束link
  • EXTERN(symbol1 symbol2 ...)
    • 強迫讓指定的symbol設成undefined,手冊說一般用在刻意要使用非標準的API。例如自幹printf時可以用這個命令。 (不過變成了undefine symbol怎麼link??)
  • FORCE_COMMON_ALLOCATION

    • 手冊和男人說和相容性有關,手冊上是說強迫分配空間給common symbols,即使是link relocate檔案。(common symbols不知道是什麼)
  • OUTPUT_ARCH(bfdarch)

    • 指定輸出的平台,可以透過objdump -i查詢支援平台
  • INSERT [ AFTER | BEFORE ] output_section
    • 指定在預設linker script命令被執行之前或是之後加上或加入特定的輸入section到輸出section。以下是一個範例
範例
1
2
3
4
5
6
7
8
9
SECTIONS
{
  OVERLAY :
  {
      .ov1 { ov1*(.text) }
      .ov2 { ov2*(.text) }
  }
}
INSERT AFTER .text;

設定symbol的值

linker script提供設定symbol數值的方法。要注意的是,這邊的symbol可以指一個全域變數、SECTION命令中的location counter(就是.開頭的資料如.text

使用方式介紹如下:

基本運算

symbol assignment operations
1
2
3
4
5
6
7
8
9
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;

關於expression是三小後面會再討論。

手冊上提供的範例

symbol assign範例
1
2
3
4
5
6
7
8
9
10
11
floating_point = 0;
SECTIONS
{
  .text :
  {
  *(.text)
  _etext = .;
  }
  _bdata = (. + 3) & ~ 3;
  .data : { *(.data) }
}

從這邊可以看到幾種assign

  • 設定全域變數floating_point的symbol為0
  • 設定全域變數_etext的值為輸入object檔案.text合體後的offset,個人猜測可以理解成end of text。(回顧一下.是offset counter)
  • 設定全域變數_bdata的值為輸出object檔案.text結尾的offset 的4的倍數位址。這邊透露兩個資訊
    • 個人猜測可以理解成begin of data
    • 四的倍數和alignment的問題應該有關聯。

HIDDEN命令

  • HIDDEN(要隱藏的symbol) 可以把他理解成加了static的全域變數,也就是說這個symbol只在這個處理範圍中才能摸到。

PROVIDE命令

  • PROVIDE命令(symbol = expression)
    • 簡單來說,如果你的程式已經有這個symbol(函數或變數),就用你的;否則就使用這邊提供的symbol。手冊上說是給特殊的linker使用的。想知道他提的use case可以看這邊,我是沒什麼感覺。

PROVIDE_HIDDEN命令

  • PROVIDE_HIDDEN(symbol = expression)
    • 和PROVIDE命令相同,差別是這個symbol只在這個處理範圍中才能摸到,一如HIDDEN命令。

談談source code和linker script symbol的關係

這節很有趣,解答我的一些小問題。

  • 變數如何存放在binary中?
    • 先把變數名稱放入symbol table內,換句話說symbol table會多一筆資料。這筆資料的欄位有
      • symbol的位址
      • symbol的flags
      • symbol屬於哪個SECTION
      • symbol佔的記憶體空間或是alignment規範
      • symbol的名稱
    • 典型的symbol table 資料:C語言的main()
      *  `00000000004005ed g     F .text  0000000000000101              main`
      
    • 而symbol的flag有7個groups
      • Group 1:
        • l: local
        • g: global
        • u: unique global,GNU 用於ELF時的 symbol binding extenstion
        • !: 既是global也是local
      • Group 2:
        • w: weak symbol
        • <空白>: strong symbol
      • Group 3:
        • C: symbol 是一個constructor (不知道這邊constructor是指那個東西? )
        • <空白>: 一般 symbol
      • Group 4:
        • W: warning symbol (不知道是三小)
        • <空白>: 一般 symbol
      • Group 5:
        • I: 間接地reference其他的symbol
        • i: relocate 時要處理的function
        • <空白>: 一般 symbol
      • Group 6:
        • D: dynamic symbol (不知道是三小)
        • d: debug symbol
        • <空白>: 一般 symbol
      • Group 7:
        • F: 這是一個function
        • f: 這是一個檔案
        • O: 這是一個object
        • <空白>: 一般 symbol
    • 如果有初始值,順便設定初始值。
  • 程式語言的取值foo = 100 runtime發生什麼事?
    • 先去symbol table找foo存在記憶體的位址,把那個位址依照symbol table的size規則將100寫入該位址。
  • 程式語言的取值ptr = &foo runtime發生什麼事?
    • 先去symbol table找foo存在記憶體的位址,把那個位址寫到ptr在symbol table對應的記憶體。
  • symbol在symbol table中存放第一個欄位是symbol的值,而這個值是一個位址
  • 在linker script設定的symbol如foo = 100和在程式碼中轉出的symbol如foo = 100差別在那?
    • linker script的100代表的是symbol的位址,而程式碼中轉出的symbol的100代表的是foo對應記憶體存放的值。
  • 如何從C語言程式碼中摸到linker script內定義的symbol?
  • 我可以反方向從linker script摸程式碼的symbol
    • 不一定,不同的程式語言和編譯器有不同的變數和函數命名方式,也就是說你原始程式碼的symbol名稱可能不是最後存在輸出object檔案的symbol 名稱。

SETCION命令

其實一開始是為了看懂這個命令才會想看linker script的。如果接觸過很小型的Embedded OS就會發現很多都是自幹linker script;而這些scripts主要的描述命令就是SETCION

好了,廢話少說,進入主題。SECTION命令的功用是

  • 告訴linker怎麼把輸入object檔案中的SECTION對應到輸出object檔案中的SECTION
  • 告訴loader object檔案中的SECTION要放到記憶體那些地方

典型的SECTION命令長這樣子:

1
2
3
4
5
6
SECTIONS
{
  sections-command
  sections-command
  ...
}

望文生義地猜測可以這樣理解: 輸出object有一些大方向的規範,並且分為不同的section,每個section有他自己的規範。

sections-command可以分為下面幾種功能

要注意的事,如果你自幹的linker script沒有描述輸出object檔案的setcion的話,linker會

  • 讀輸入object檔案section時,如果該section第一次出現,就在輸出object檔案中加入同樣名稱的section,直到處理完所有的輸入object檔案
  • 第一個吃到的輸入object檔案section將當作位址0的起始點

輸出object檔案的section描述

輸出object檔案的section描述格式
1
2
3
4
5
6
7
8
9
10
section [address] [(type)] :
  [AT(lma)]
  [ALIGN(section_align) | ALIGN_WITH_INPUT]
  [SUBALIGN(subsection_align)]
  [constraint]
  {
      output-section-command
      output-section-command
      ...
  } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

其中output-section-command的功能有

  • 設定symbol的值
  • 描述輸入object檔案中的section要怎麼放到輸出object檔案的setcion
  • 輸出object檔案的setcion的資料存放格式如alignment等
  • 其他

這邊很多術語需要先搞清楚,先列出來,希望之後可以看到解答

  • type
  • region
  • AT(lma)
  • lma_region
  • =fillexp

輸出object檔案的section 命名

  • 必須符合你要輸出object檔案binary format規定。

輸出object檔案section 命令: address欄位

address是section的一個optional欄位,使用的記憶體空間為VMA。如果沒有指定的話,linker會依下面的方式設定輸出object檔案section 的VMA。該VMA會遵循section 的alignment規範。

  • 有設定region的話就從region內剩餘空間開始位址
  • 有使用MEMORY命令定義硬體記憶區塊的話,從定義的區塊中挑第一個符合SECTION的區塊。再將address設成該區塊內剩餘空間開始位址
  • 以上皆非的情況下,位址設成locale counter

address欄位因為可以使用exression所以可能有下面的陷阱

  • .text . : { *(.text) }
  • .text : { *(.text) } 這兩個差一個.,意義就差很多。沒有.那個,表示沒有設定address,所以就是設成locale counter,並且linker會保證alignment。而有.的就表示hardcode成locale counter,所以有可能會有alignment的問題。

另外一點要注意的設定後locale counter也會跟著改變。

輸入object檔案的section描述

這部份可以說是整個output-section-command的重點,目的是告訴linker讀取輸入object檔案後,怎麼把這些檔案裏面的section複製到輸出object檔案裏面適當地section。

輸入object檔案的section 基礎概念

格式為檔案(section1 section2 ...),檔案支援萬用字元

所以常看到的*(.text)的意思是:所有輸入object檔案裏面的.text section。

指定多個section的方式有兩種

  • *(.sec1 .sec2):如果輸入object有兩個檔案的話,輸出object檔案裏面section會變成 ld1_section.png

  • *(.sec1) *(.sec2): 如果輸入object有兩個檔案的話,輸出object檔案裏面section會變成 ld2_section2.png


  • 待釐清項目
    • dynamic symbol (不知道是三小)
    • warning symbol (不知道是三小)
    • constructor symbol (不知道是三小)
      • Overlay描述 (不知道是三小)
  • 概念
    • 名詞解釋
      • bss
      • text
      • data
    • locale counter
    • region

參考資料

Makefile的Pattern Rules小小注意事項

| Comments

Makefile 自動將產生的檔案指定到特定目錄中,我犯了一個錯誤,造成更動header檔案發生錯誤(原文已修復)。

狀況如下

Error log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ make
mkdir -p build/libs/
cc -g -MMD -I ./include -c libs/liba.c -o build/libs/liba.o
mkdir -p build/libs/
cc -g -MMD -I ./include -c libs/libb.c -o build/libs/libb.o
mkdir -p build/
cc -g -MMD -I ./include -c test.c -o build/test.o
cc -o build/test build/libs/liba.o build/libs/libb.o build/test.o

$ make
make: `build/test' is up to date.

$ touch include/*

$ make
mkdir -p build/libs/
cc -g -MMD -I ./include -c libs/liba.c include/libb.h -o build/libs/liba.o
cc: fatal error: cannot specify -o with -c, -S or -E with multiple files
compilation terminated.
make: *** [build/libs/liba.o] Error 4

從錯誤中可以看到,明明在編liba.c code怎麼會有libb.h來亂呢?請再看下面的分析。

有問題的Makefile如下

有問題的Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CFLAGS=-g -MMD -I ./include

# binaries
LIB_SRC=$(shell ls libs/*.c)
SRCS= $(LIB_SRC) test.c

OBJS = $(patsubst %.c, %.o, $(SRCS))

# Output
OUT_DIR = build
OUT_OBJS=$(addprefix $(OUT_DIR)/, $(OBJS))
DEPS = $(patsubst %.o, %.d, $(OUT_OBJS))

# Build Rules
TARGET=test

$(OUT_DIR)/$(TARGET): $(OUT_OBJS)
  $(CC) -o $@ $^

$(OUT_DIR)/%.o:%.c
  mkdir -p $(dir $@)
  $(CC) $(CFLAGS) -c $^ -o $@

clean:
  rm -rf $(OUT_DIR)

-include $(DEPS)

問題出在$(CC) $(CFLAGS) -c $^ -o $@,再次回顧一下

  • -MMD效果
有問題的Makefile
1
2
$ cat build/libs/liba.d
build/libs/liba.o: libs/liba.c include/libb.h

而在Makefile中我們會使用-include $(DEPS) include 這些*.d檔案,所以更改Makefile部份敘述。

1
2
3
$(OUT_DIR)/%.o:%.c
  mkdir -p $(dir $@)
  $(CC) $(CFLAGS) -c $^ -o $@

第一次make後,當header改變, 有的prerequisite會更動,進而觸發Pattern Rule,但是$^是所有的prerequisite,所以$(CC)會把所有的prerequisite全部帶入造成錯誤。已這邊的例子,prerequisite就是libs/liba.cinclude/libb.h

解法很簡單,把$^換成$<就可以了。而$<是什麼呢?就是第一個prerequisite,對照這邊可以看到,第一個就是第一個prerequisite就是libs/liba.c檔案了。

參考資料