« November 2006 |
(回到Blog入口)
| January 2007 »
December 2006 歸檔
TimeSys 是一家「供應現成 RFS」的服務公司,他們每個月都會有幾場 Webinars 的活動,本月份(12月份)的 Webinars 一共三場:
1. Linux Boot-Up:了解由 bootloader 開機到 user-space 模式,並執行 user program 的過程。
2. Hello World, from an Embedded Perspective:怎麼把 "Hello World" 做 cross compilation 後放到 RFS。
3. Survey of Linux Filesystems:介紹 embedded system 使用的 filesystem,以及建立 RFS 的方法。
這三個主題都是給初學者的議題,有興趣的朋友一定要撥空上網參加喔!參加 TimeSys 的 Webinars 必須事前註冊,有興趣的朋友可至 TimeSys 的註冊頁面登記。
在註冊頁面有每一場 webinar 的時間與大綱。
System call 是 process 與作業系統之間的介面。System call 是由 Linux kernel
所實作並提供給使用者,user-space program 可透過 system call 與Linux kernel 溝通。以 C 語言來呼叫 system call 的話,則是透過 GLIBC(libc)來間接呼叫。
GLIBC 提供呼叫 system call 的介面稱為 wrapper routine,wrapper routine 會叫起 Linux 的
system call handler,最後再由 system call handler 找到service routine 的所在的位址,並交由
service routine 完成工作。整個流程如圖所示。
User program 與 wrapper routine 是 user-space 的 code,system call handler 與
service routine 則是屬於 kernel space。如圖,我們可以看到灰色的箭頭跨越一個鴻溝,代表著由 user space 進入 kernel space,在 i386 上這個動作是藉由中斷來達成。
在 Linux system 底下,必須透過 GLIBC 裡的 system call function 來取得 kernel 的 system call 服務。
由 GLIBC 提供用來呼叫 system call 的函數稱為 wrapper function,wrapper function會呼叫 Linux kernel的handler routine,即 system_call() 函數。
呼叫 getpid() system call 的範例:
#include
#include
#include
int main()
{
pid_t self, parent;
self = getpid();
parent = getppid();
printf("PID: %d, Parent PID: %d\n", (int) self, (int) parent);
return 0;
}
利用 strace 工具可以追蹤程式所使用到的 system call:
$ strace ./pid
execve("./pid", ["./pid"], [/* 25 vars */]) = 0
uname({sys="Linux", node="jollen", ...}) = 0
brk(0) = 0x80495ac
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40016000
open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=114793, ...}) = 0
old_mmap(NULL, 114793, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000
close(3) = 0
open("/lib/tls/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0`V\1B4\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1531064, ...}) = 0
old_mmap(0x42000000, 1257224, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x42000000
old_mmap(0x4212e000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x12e000) = 0x4212e000
old_mmap(0x42131000, 7944, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x42131000
close(3) = 0
set_thread_area({entry_number:-1 -> 6, base_addr:0x400169e0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
munmap(0x40017000, 114793) = 0
getpid() = 970
getppid() = 969
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000
write(1, "PID: 970, Parent PID: 969\n", 26) = 26
munmap(0x40017000, 4096) = 0
exit_group(0) = ?
由 strace 的輸出結果可以得知程式所呼叫的 system call,在這裡我們可以看到 getpid() 與 getppid(),並且也能得知 printf() 是基於 write() system call。
Wrapper routine 透過 0x80 號中斷(軟體中斷)進入 kernel space,並且跳至 system call
handler執行。由於透過軟體中斷即可達成呼叫 system call的目地,因此使用 assembly來寫程式時,我們也可以直接呼叫system call。
在產生軟體中斷前,我們必須告訴 Linux kernel 所要呼叫的 system call 編號,system call 的編號透過 %eax
暫存器來指定;若要傳遞參數,則是透過其它暫存器來傳遞。
參數傳遞與傳回值
Linux system call最多可傳遞6個參數,參數的傳遞是透過以下的暫存器來完成:
- %ebx:第1個參數。
- %ecx:第2個參數。
- %edx:第3個參數。
- %esi:第4個參數。
- %edi:第5個參數。
- %ebp:第6個參數(做臨時用途)。
System call 的編號由 %eax 暫存器指定。System call 的傳回值則是存放於 %eax
暫存器。請看以下的範例程式 hello.S。
.data
msg: .ascii "Hello, World!\n"
len = . – msg
.text
.global _start
_start:
# sys_write
movl $len,%edx
movl $msg,%ecx
movl $1,%ebx
movl $4,%eax
int $0x80
# sys_exit
movl $0,%ebx
movl $1,%eax
int $0x80
組譯與連結的方法為:
$ as -o hello.o hello.S
$ ld -s -o hello hello.o
透過 assembly 程式,我們便能來初步了解 x86 上的 system call 架構。
我們由之前的 hello.S 程式範例來做切入:
movl $len,%edx
movl $msg,%ecx
movl $1,%ebx
movl $4,%eax
int $0x80
System call 編號 4(%eax = 4)為 sys_write 函數,其原型宣告如下:
ssize_t sys_write(unsigned int fd, const char * buf, size_t count)
根據 sys_write 的參數宣告,我們必須傳遞參數如下:
˙ 第 1 個參數為 unsigned int fd,透過 %ebx 暫存器傳值。
˙ 第 2 個參數 const char * buf 透過 %ecx 暫存器傳遞位址(address)。
˙ 第 3 個參數 size_t count 透過 %edx 暫存器傳值。
這個部份的話嘛,大家不妨翻閱一下 Jollen 整理的
LSCT!
x86 的 Interrupt
x86的interrupt(中斷)可分別系統定義與使用者自訂:
˙ 中斷向量0~8、10~14、16~18:predefined interrupts and exceptions。
˙ 中斷向量19-31:保留。
˙ 中斷向量32-255:user-defined interrupts(maskable interrupts)。
每個中斷都有一個編號,稱為interrupt vector(中斷向量);當中斷產生後,就會跳至相對應的interrupt
handler執行程式。Interrupt handler程式的所在位址經由interrupt descriptor table(IDT)來得知。
Enternal interrupt、software interrupts與exception都是透過IDT來處理,IDT裡存放的是gate
descriptor,gate descriptor的種類有:
˙ interrupt gate
˙ trap gate
˙ task gate
中斷的處理過程如下:
1. 中斷產生。
2. CPU根據interrupt vector來索引IDT裡的gate descriptor。
3. 如果 gate descriptor 是 interrupt gate,trap gate則以類似call gate方式呼叫handler
procedure。如果是task gate,則透過task switch 來呼叫handler。
Interrupt gate與task gate的主要差別在於interrupt gate會將EFLAG的IP位元清除,而task gate則不會去變更IP位元。Linux不使用task gate。
System call 是透過中斷來呼叫,而在 x86 系統的架構中,32-255 是所謂的 maskable
interrupts 即使用者定義的中斷。Linux 在 i386 上實作system call可透過以下 2 個機制:
˙ lcall7/lcall27(call gates,呼叫閘道)
˙ int 0x80(software interrupt.軟體中斷)
Linux 應用程式使用 int 指令來觸發 0x80 號軟體中斷,其它作業系統像是 Solaris 的應用程式,則是使用 lcall7。在開機時,IDT是由arch/i386/kernel/traps.c:trap_init()
做初始化的設定:
void __init trap_init(void)
{
#ifdef CONFIG_EISA
if (isa_readl(0x0FFFD9) == 'E'+('I'<<8)+('S'<<16)+('A'<<24))
EISA_bus = 1;
#endif
#ifdef CONFIG_X86_LOCAL_APIC
init_apic_mappings();
#endif
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_intr_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
set_trap_gate(18,&machine_check);
set_trap_gate(19,&simd_coprocessor_error);
set_system_gate(SYSCALL_VECTOR,&system_call);
/*
* default LDT is a single-entry callgate to lcall7 for iBCS
* and a callgate to lcall27 for Solaris/x86 binaries
*/
set_call_gate(&default_ldt[0],lcall7);
set_call_gate(&default_ldt[4],lcall27);
/*
* Should be a barrier for any external CPU state.
*/
cpu_init();
#ifdef CONFIG_X86_VISWS_APIC
superio_init();
lithium_init();
cobalt_init();
#endif
}
由以上的程式碼可以看出,0x80 號中斷向量會指到 system_call 進入點的位址。system_call 位於
arch/i386/kernel/entry.S:
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
testb $0x02,tsk_ptrace(%ebx) # PT_TRACESYS
jne tracesys
cmpl $(NR_syscalls),%eax
jae badsys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
movl %eax,EAX(%esp) # save the return value
ENTRY(ret_from_sys_call)
cli # need_resched and signals atomic test
cmpl $0,need_resched(%ebx)
jne reschedule
cmpl $0,sigpending(%ebx)
jne signal_return
0x80 號中斷裡有很多 system call service routine,每個 system call service routine
的位址是經由 sys_call_table 查表得知。要呼叫 system call 時,必須告訴 Linux kernel 該 system call 的編號。System call 的編號透過 %eax 暫存器來傳遞給 Linux kernel。
例如 sys_open 這個 system call 的編號為 5。因此當我們呼叫 sys_open() 時,就要指定
%eax 暫存器的值為5。
unistd.h 是一個重要的標頭檔,裡頭是 system call 編號的定義;另外,linux/arch/i386/kernel/entry.S
則是每一個 system call 的進入點,也就是 system call table(位於 .data section)。
unistd.h也定義了處理不同參數個數的 system call handler,在這個標頭檔裡可以看到處理 0~6個參數的
handler(_syscall0~_syscall6)。例如以下是處理 1 個參數的handler:
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \
__syscall_return(type,__res); \
}
type, name分別為 system call的傳回值型別與函數名稱,例如呼叫 fork(),則此巨集展開後會變:
int fork(type 1 arg1)
{
…
}
LPIC 證照要「重考」了,新的重新認證規章己經正式發佈了;由取得認證日起算,在二年後就要準備重新認證的程序。
LPI 會在取得認證二年後建議做重新認證(recertification)的動作,若要讓取得的 LPIC 認證有效(ACTIVE),就必須在五年內完成重新認證的工作。重新認證必須通過持有最高認證等級的所有最新考試,通過重新認證後,證書的狀態便會更新為 ACTIVE,有效期間為五年。
當取得較高等級的認證後,由取得較高等級之日起算,所有較低等級的認證狀態都會變更為 ACTIVE,期限一樣是五年。若沒有重新進行認證考試,並且讓認證失效的話,那麼就要重新取得所有認證,包含目前的最高等級與較低等級的認證。
LPI 由 2004 年 9 月 1 日開始,啟用了認證狀態資料庫,透過此資料庫能查詢證照的狀態(ACTIVE 或 INACTIVE),以往在此日期以前所取得的認證都被視為終生有效,不過在新的規章裡,LPI 將不再賦與該早期認證的終生有效權利。在 2004 年 9 月 1 日前取得認證的朋友,仍然要根據最新訂定的規章乖乖進行認證的工作。
因此,LPIC 認證將不再終生有效,原先的「終生版本」將由取得認證之日算起五年內有效。所以如果你的 LPIC 證照是在 2004 年 9 月 1 日之後取得的話,照這樣算來,就要在 2009 年 9 月 1 日前重新認證完畢;那如果是更早期取得的認證不就完蛋了嗎,很可能再過幾天就到期了啊!
不過,在 LPI 於 2006 年 12 月 1 日(美國時間)發表的新規章裡提到,在 2003 年 9 月 1 日之前取得的認證,只要在 2008 年 9 月 1 日前重新認證即可,這段期間內,證照仍視為有效(ACTIVE)。
目前已經有 LPIC 認證,或是準備要考取認證的朋友,可要留意了。「規章原文」
範例列表:loader v0.3
/*
* Copyright(c) 2003,2006 www.jollen.org
*
* ELF programming. ver 0.3
*
*/
#include <stdio.h>
#include <unistd.h>
#include <elf.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int elf_ident(char *ident)
{
if (*(ident+EI_MAG0) != ELFMAG0) return 0;
if (*(ident+EI_MAG1) != ELFMAG1) return 0;
if (*(ident+EI_MAG2) != ELFMAG2) return 0;
if (*(ident+EI_MAG3) != ELFMAG3) return 0;
return -1;
}
void parse_ident(char *ident)
{
printf("ELF Identification\n");
printf(" Class: ");
switch (*(ident+EI_CLASS)) {
case ELFCLASSNONE: printf("Invalid class\n"); break;
case ELFCLASS32: printf("32-bit objects\n"); break;
case ELFCLASS64: printf("64-bit objects\n"); break;
}
}
void parse_machine(Elf32_Half machine)
{
printf("Machine: ");
switch (machine) {
case EM_NONE: printf("No machine\n"); break;
case EM_M32: printf("AT&T WE 32100\n"); break;
case EM_SPARC: printf("SPARC\n"); break;
case EM_386: printf("Intel 80386\n"); break;
case EM_68K: printf("Motorola 68000\n"); break;
case EM_88K: printf("Motorola 88000\n"); break;
case EM_860: printf("Intel 80860\n"); break;
case EM_MIPS: printf("MIPS RS3000 Big-Endian\n"); break;
default: printf("Unknow\n");
}
}
void parse_sections(Elf32_Ehdr *hdr, int fd)
{
int i;
Elf32_Shdr header[40];
Elf32_Shdr *strtab; /* point to string table */
printf("Num of secionts: %d\n", hdr->e_shnum);
/* file offset of section header table */
lseek(fd, hdr->e_shoff, SEEK_SET);
for (i = 0; i < hdr->e_shnum; i++) {
read(fd, &header[i], sizeof(Elf32_Shdr));
/* find out string table ! */
if (header[i].sh_type == SHT_STRTAB) strtab = &header[i];
}
}
int main(int argc, char *argv[])
{
int fd;
Elf32_Ehdr f_header;
if (argc != 2) {
printf("Usage: loader [filename]\n");
return -1;
}
fd = open(argv[1], S_IRUSR);
if (fd < 0) {
printf("\nfile open error\n");
return -1;
}
/* Read ELF Header */
read(fd, &f_header, sizeof(Elf32_Ehdr));
/* Parse header information */
if (elf_ident(f_header.e_ident)) {
parse_ident(f_header.e_ident);
parse_machine(f_header.e_machine);
parse_sections(&f_header, fd);
} else {
printf("not a ELF binary file\n");
}
close(fd);
}
執行結果
$ ./loader-0.3 ./loader-0.3
ELF Identification
Class: 32-bit objects
Machine: Intel 80386
Num of secionts: 34
根據 SysV ABI 的定義,若 section 的類型為 SHT_STRTAB,則該 section entry 即為 string
table 的 section header。Section 的類型可由 section header 的 sh_type 欄位來判斷,SysV
ABI 定義的 section 類型(sh_type)如下表所示。
表 sh_type 欄位的定義
| Name |
Value |
| SHT_NULL |
0 |
| SHT_PROGBITS |
1 |
| SHT_SYMTAB |
2 |
| SHT_STRTAB |
3 |
| SHT_RELA |
4 |
| SHT_HASH |
5 |
| SHT_DYNAMIC |
6 |
| SHT_NOTE |
7 |
| SHT_NOBITS |
8 |
| SHT_REL |
9 |
| SHT_SHLIB |
10 |
| SHT_DYNSYM |
11 |
| SHT_LOPROC |
0x70000000 |
| SHT_HIPROC |
0x7fffffff |
| SHT_LOUSER |
0x80000000 |
| SHT_HIUSER |
0xffffffff |
我們在範例程式 loader-0.3.c 中,試著由一堆的 section 裡找出類型為 string table(SHT_STRTAB)的section。接下來程式
loader-0.4.c 將會試著實作讀取 ELF 的 string table,並將有 section 的名稱印出。
讀取 Section Name String Table
String table 是一個特殊的 section,此 section 紀錄所有 section 的名稱(ASCII 字串)。String table
是一個字元型別的陣列,每一個 section 都會有一個索引值來索引自己的 section name 字串,section header 的
sh_name 欄位則是存放了此索引值,如圖所示。
圖 ELF section name string table
接下來的範例程式將不再直接列表,請大家由
http://tw.jollen.org/elf-programming 下載。
程式說明
呼叫了 parse_sections() 函數來讀取section header table:
parse_sections(&f_header, fd);
相較於 loader-0.3.c 的 parse_sections() 函數,在 0.4 的版本裡,我們所做的改變如下:
1. 讀取 string table 的內容
2. 列印所有 section 的名稱
首先,我們新增一個陣列來存放 string table 的內容:
char strtab[65535];
接下來一樣讀取所有的 section entry,並且找出 string table:
for (i = 0; i < hdr->e_shnum; i++) {
read(fd, &header_ent[i], sizeof(Elf32_Shdr));
/* load section name string table */
if (i == hdr->e_shstrndx) {
sh_strtab = &header_ent[i];
}
}
我們試著用另外一種方法來找出 string table 吧!根據 SysV ABI 的定義,string table 在 section header
table 裡的 section entry index(索引值)紀錄在 ELF header 的 e_shstrndx
欄位,因此,我們判斷目前的 section header table 索引值是否等於 e_shstrndx 來找出 string
table。接下來再讀取string table 的內容:
/* read “String Table” */
lseek(fd, sh_strtab->sh_offset, SEEK_SET);
read(fd, strtab, sh_strtab->sh_size);
程式裡利用 lseek() 函數將檔案讀寫指標移 string table 開始的地方,然後再將整個 string table
讀出。要注意的是,section 的長度紀錄於 section header裡的 sh_size
欄位。最後再逐一將每個section的名稱列印在螢幕上:
/* Index 0: undefined */
for (i = 1; i < hdr->e_shnum; i++) {
printf("%s\n", &strtab[header_ent[i].sh_name]);
}
小結
我們知道一個很重要的觀念了。ELF section 的字串名稱是由 string table 查表得知,section 名稱在 string table
陣列裡的索引值則是紀錄在 section header 裡的 sh_name 欄位。
我們接續 loader v0.4 的工作,強化一下輸出結果的可讀性;先來比較一下 loader v0.4 與 loader v0.5
的輸出畫面。
| $ ./loader-0.4 loader-0.4 |
$ ./loader-0.5 loader-0.4 |
ELF Identification
Class: 32-bit objects
Machine: Intel 80386
Num of secionts: 34
.interp
.note.ABI-tag
.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.plt
.text
.fini
.rodata
.eh_frame
.data
.dynamic
.ctors
.dtors
.jcr
.got
.bss
.comment
.debug_aranges
.debug_pubnames
.debug_info
.debug_abbrev
.debug_line
.debug_frame
.debug_str
.shstrtab
.symtab
.strtab
|
ELF Identification
Class: 32-bit objects
Machine: Intel 80386
Name Size FileOff
[01] .interp 19 244
[02] .note.ABI-tag 32 264
[03] .hash 56 296
[04] .dynsym 144 352
[05] .dynstr 98 496
[06] .gnu.version 18 594
[07] .gnu.version_r 32 612
[08] .rel.dyn 8 644
[09] .rel.plt 48 652
[10] .init 23 700
[11] .plt 112 724
[12] .text 1292 836
[13] .fini 27 2128
[14] .rodata 348 2156
[15] .eh_frame 4 2504
[16] .data 12 2508
[17] .dynamic 200 2520
[18] .ctors 8 2720
[19] .dtors 8 2728
[20] .jcr 4 2736
[21] .got 40 2740
[22] .bss 4 2780
[23] .comment 306 2780
[24] .debug_aranges 120 3088
[25] .debug_pubnames 37 3208
[26] .debug_info 2692 3245
[27] .debug_abbrev 312 5937
[28] .debug_line 636 6249
[29] .debug_frame 20 6888
[30] .debug_str 1722 6908
[31] .shstrtab 299 8630
[32] .symtab 1856 10292
[33] .strtab 1132 12148
|
Cool!做出了 'objdump -x' 的部份功能。頗為有趣,那麼用 objdump 來互相比較一下看看。
loader v0.5 v.s. objdump v.s. readelf
| $ objdump -x loader-0.4(只擷取 section 輸出結果) |
$ ./loader-0.5 loader-0.4 |
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 00000013 080480f4 080480f4 000000f4 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 08048108 08048108 00000108 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .hash 00000038 08048128 08048128 00000128 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .dynsym 00000090 08048160 08048160 00000160 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .dynstr 00000062 080481f0 080481f0 000001f0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .gnu.version 00000012 08048252 08048252 00000252 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .gnu.version_r 00000020 08048264 08048264 00000264 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .rel.dyn 00000008 08048284 08048284 00000284 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .rel.plt 00000030 0804828c 0804828c 0000028c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .init 00000017 080482bc 080482bc 000002bc 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
10 .plt 00000070 080482d4 080482d4 000002d4 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .text 0000050c 08048344 08048344 00000344 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .fini 0000001b 08048850 08048850 00000850 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .rodata 0000015c 0804886c 0804886c 0000086c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
14 .eh_frame 00000004 080489c8 080489c8 000009c8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
15 .data 0000000c 080499cc 080499cc 000009cc 2**2
CONTENTS, ALLOC, LOAD, DATA
16 .dynamic 000000c8 080499d8 080499d8 000009d8 2**2
CONTENTS, ALLOC, LOAD, DATA
17 .ctors 00000008 08049aa0 08049aa0 00000aa0 2**2
CONTENTS, ALLOC, LOAD, DATA
18 .dtors 00000008 08049aa8 08049aa8 00000aa8 2**2
CONTENTS, ALLOC, LOAD, DATA
19 .jcr 00000004 08049ab0 08049ab0 00000ab0 2**2
CONTENTS, ALLOC, LOAD, DATA
20 .got 00000028 08049ab4 08049ab4 00000ab4 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .bss 00000004 08049adc 08049adc 00000adc 2**2
ALLOC
22 .comment 00000132 00000000 00000000 00000adc 2**0
CONTENTS, READONLY
23 .debug_aranges 00000078 00000000 00000000 00000c10 2**3
CONTENTS, READONLY, DEBUGGING
24 .debug_pubnames 00000025 00000000 00000000 00000c88 2**0
CONTENTS, READONLY, DEBUGGING
25 .debug_info 00000a84 00000000 00000000 00000cad 2**0
CONTENTS, READONLY, DEBUGGING
26 .debug_abbrev 00000138 00000000 00000000 00001731 2**0
CONTENTS, READONLY, DEBUGGING
27 .debug_line 0000027c 00000000 00000000 00001869 2**0
CONTENTS, READONLY, DEBUGGING
28 .debug_frame 00000014 00000000 00000000 00001ae8 2**2
CONTENTS, READONLY, DEBUGGING
29 .debug_str 000006ba 00000000 00000000 00001afc 2**0
CONTENTS, READONLY, DEBUGGING
|
ELF Identification
Class: 32-bit objects
Machine: Intel 80386
Name Size FileOff
[00] .interp 19 244
[01] .note.ABI-tag 32 264
[02] .hash 56 296
[03] .dynsym 144 352
[04] .dynstr 98 496
[05] .gnu.version 18 594
[06] .gnu.version_r 32 612
[07] .rel.dyn 8 644
[08] .rel.plt 48 652
[09] .init 23 700
[10] .plt 112 724
[11] .text 1292 836
[12] .fini 27 2128
[13] .rodata 348 2156
[14] .eh_frame 4 2504
[15] .data 12 2508
[16] .dynamic 200 2520
[17] .ctors 8 2720
[18] .dtors 8 2728
[19] .jcr 4 2736
[20] .got 40 2740
[21] .bss 4 2780
[22] .comment 306 2780
[23] .debug_aranges 120 3088
[24] .debug_pubnames 37 3208
[25] .debug_info 2692 3245
[26] .debug_abbrev 312 5937
[27] .debug_line 636 6249
[28] .debug_frame 20 6888
[29] .debug_str 1722 6908
[30] .shstrtab 299 8630
[31] .symtab 1856 10292
[32] .strtab 1132 12148
|
不過事情好像有點怪異,我們的 loader v0.5 怎麼在最後多出 3 個 section 呢?被附身了。
不過,原來是 objdump 不會把最後這 3 個 section 印出來啦,這 3 個 section 分別是:
1) .shstrtab:section header straing table
2) .symtab:symbol table
3) .strtab:string table
哇塞!原來 symbol table 藏在這裡啊。但是,如果我們改用 readelf 來讀 ELF,就可以看到完整的 ELF headers
了,而且輸出的畫面也比較具可讀性:
$ readelf -S loader-0.4
There are 34 section headers, starting at offset 0x22e4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048108 000108 000020 00 A 0 0 4
[ 3] .hash HASH 08048128 000128 000038 04 A 4 0 4
[ 4] .dynsym DYNSYM 08048160 000160 000090 10 A 5 1 4
[ 5] .dynstr STRTAB 080481f0 0001f0 000062 00 A 0 0 1
[ 6] .gnu.version VERSYM 08048252 000252 000012 02 A 4 0 2
[ 7] .gnu.version_r VERNEED 08048264 000264 000020 00 A 5 1 4
[ 8] .rel.dyn REL 08048284 000284 000008 08 A 4 0 4
[ 9] .rel.plt REL 0804828c 00028c 000030 08 A 4 b 4
[10] .init PROGBITS 080482bc 0002bc 000017 00 AX 0 0 4
[11] .plt PROGBITS 080482d4 0002d4 000070 04 AX 0 0 4
[12] .text PROGBITS 08048344 000344 00050c 00 AX 0 0 4
[13] .fini PROGBITS 08048850 000850 00001b 00 AX 0 0 4
[14] .rodata PROGBITS 0804886c 00086c 00015c 00 A 0 0 4
[15] .eh_frame PROGBITS 080489c8 0009c8 000004 00 A 0 0 4
[16] .data PROGBITS 080499cc 0009cc 00000c 00 WA 0 0 4
[17] .dynamic DYNAMIC 080499d8 0009d8 0000c8 08 WA 5 0 4
[18] .ctors PROGBITS 08049aa0 000aa0 000008 00 WA 0 0 4
[19] .dtors PROGBITS 08049aa8 000aa8 000008 00 WA 0 0 4
[20] .jcr PROGBITS 08049ab0 000ab0 000004 00 WA 0 0 4
[21] .got PROGBITS 08049ab4 000ab4 000028 04 WA 0 0 4
[22] .bss NOBITS 08049adc 000adc 000004 00 WA 0 0 4
[23] .comment PROGBITS 00000000 000adc 000132 00 0 0 1
[24] .debug_aranges PROGBITS 00000000 000c10 000078 00 0 0 8
[25] .debug_pubnames PROGBITS 00000000 000c88 000025 00 0 0 1
[26] .debug_info PROGBITS 00000000 000cad 000a84 00 0 0 1
[27] .debug_abbrev PROGBITS 00000000 001731 000138 00 0 0 1
[28] .debug_line PROGBITS 00000000 001869 00027c 00 0 0 1
[29] .debug_frame PROGBITS 00000000 001ae8 000014 00 0 0 4
[30] .debug_str PROGBITS 00000000 001afc 0006ba 01 MS 0 0 1
[31] .shstrtab STRTAB 00000000 0021b6 00012b 00 0 0 1
[32] .symtab SYMTAB 00000000 002834 000740 10 33 54 4
[33] .strtab STRTAB 00000000 002f74 00046c 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
原來這是工具的問題。嗯!好吧,以後都改用 readelf 來玩吧。
Index 0: undefined
還有一點就是,ELF headers index 0 在 SysV ABI 裡的定義是「undefined」,readelf
把它輸出成 NULL,我們的範例跟 objdump 一樣,直接跳過!
readelf 輸出的其它 column 分述如下。
Type
「type」欄位的話就是在「ELF(Executable
and Linking Format)格式教學文件, #7: 讀 ELF 的 Section Name(透過 strtab)」裡介紹到的
sh_type。
Addr
這個好玩,不過目前先跳過。(>_<)
Off、Size
跟我們的 loader v0.5 輸出結果一樣,分別代表 section 在 ELF object 裡的 file offset
與其大小(bytes)。readelf 是用十六進位輸出,我們的範例是用十進位輸出。
範例程式一樣可以由「http://tw.jollen.org/elf-programming/」下載。
今天在「苦牢之最後一年」的 blog 裡看到這篇:「開根號倒數 (InvSqrt(), 1 / sqrt(x)) 速算法」,標題讓我大感興趣,原來是一篇 slashdot 上的新聞,他們在找「開根號倒數」這個利害技巧的原作者,詳細介紹可看苦牢兄的 blog。Slashdot 的新聞所提到的「找人」過程在這裡:http://www.beyond3d.com/articles/fastinvsqrt/。
仔細閱讀的話發現還相當有趣,作者是誰最後仍然不可考,被點名的高手都說不是他幹的!不過最大的收獲是知道了一份「HAKMEM」的文件,這是在 60's 年代到 70's 年代初期,一群 MIT 的高手所記錄的程式寫作技巧(tricks)。
長久以來一直被「分層架構」與「抽象化設計」所教育著,便遺忘了這些原本才是「程式設計師」本質的數理應用,只能說 programmer 稱不上,充其量可能只是看文件寫 code 的 coding mahcine 吧。
另外,講到程式寫作技巧,就想到有趣的「解題」遊戲,又想到一位「冼鏡光」老師,冼老師的那本 C 名題精選著作是解題的必備好書。目前冼老師在 Michigan Technological University 教書,老師有個人首頁。
這是一個時常被問到的問題,由於 kernel 的變動快速,因此 Jollen 在日前便寫了一篇「讓 kernel 常在我心:探討如何與 kernel 的發展同步」的日記,內容是大略介紹如何與 kernel 的發展同步(day-by-day)。
但是,如果並不是很需要每天去注意 kernel 的動態的話,只要在每個 kernel 穩定版(stable)釋出後去看 Changlog 就好了。特別是 kernel 2.6.1x 的更新項目(update)、臭蟲修正(big fix)、新的驅動程式與 filesystem 更是以可怕的「量」在 patch,特別是最近半年的 3 個 kernel 版本(2.6.17~19),變化量真是到了油門全開的狀態,所以每天看 kernel 會是沈重的負擔。
話說回來,改變是好事。期待的是,看著這麼多的改變與越來越多的新驅動程式加入,以及企業級(enterprise-class)功能的成熟,我們已經可以拿到越來越棒的「成熟」kernel 了。誠如 Linus 在 2.6.19 stable 釋出時的玩笑話「It's one of those rare 'perfect' kernels」。
言歸正傳,如果要 keep 新 kernel 做了什麼改變,或是了解「什麼功能在幾版的 kernel 才開始有」、「某些 bug 在幾版做修正」、「這個版本是否對理器架構面做修正」等,建議可以直接由 kernelnewbies.org 做查詢,例如,我想知道 kernel 2.6.17 改了什麼東西,就可以輸入以下的 URL:
http://kernelnewbies.org/Linux_2_6_17
同理,我想知道幾天前才丟出來的 kernel 2.6.19 加了什麼、改了什麼,就輸入以下的 URL:
http://kernelnewbies.org/Linux_2_6_19
就如前面提到的,kernel 2.6.1x 的修改變化相當大,特別是在 kernel 2.6.16(大約)後,每每都有重大更新,修正範圍也「波及」到「Kernel Core」。如果工作場合與 kernel 有關係,確實有必要仔細閱讀每一個版本的 Changelog。
這本書在網路上「打書」打了很久,昨天跑去天瓏站著翻閱約一分鐘後就把它帶回來了。原因是這是一本對於 embedded Linux 的 domain issues 整理的很不錯的書,並且也具備優質原文書的特點,也就是對於概念與觀念的「文字陳述」都能不廢話的切入核心(有時看英文才能挑起一些 sense )。這就是為什麼一定要閱讀原文書的原因。

這本書沒有 step-by-step 的教學,也沒有類似「指令介紹」的條列說明。對於已經有「片斷實作經驗」的讀者來說,閱讀此書能快速得到「完整的領域知識」;會以「快速」來形容是因為這本書需要一些片斷的實作經驗,才能比較容易理解章節的關係與內文佈署,但是我特別喜歡這種風格的寫作,主要原因是能無痛且快速瀏覽完整的相關議題。
同時,這本書也能讓想學習 embedded Linux 的讀者清楚了解到 embedded Linux 所涉及的層面到底有多廣。
Linux 2.6.19 正式加入了 Atmel AVR32 architecture 的支援,這是由 Atmel 原廠所實作的 architecture-level porting。引用一段節錄至 KernelNewbies.org 的 changelog wiki 上的說明:
AVR32 is a new high-performance 32-bit RISC microprocessor core, designed for cost-sensitive embedded applications, with particular emphasis on low power consumption and high code density.
Linux for AVR32 架構的資訊可參考 The AVR32 Linux project 網站。AVR32 architecture 對於作業系統(operating system)的支援方面,除了必備的 MMU(Memory Management Unit)外,還有一個 MPU(Memory Protection Unit)單位。
MPU 允許 user 將記憶體空間切割成不同的「protection regions」,每個 region 的大小都是 user-defined 的,並且「starts at a user-defined address」。另外,節錄一小段 AVR32 architecture document 上對於 MPU 的說明如下:
The different regions can have different access privileges, cacheability attributes and bufferability attributes. The MPU does not perform any address translation.
因此,AVR32 架構文件提到,MPU 是簡單的 MMU,只是不做 address translation。
幾天前 LPI(Linux Professional Institute) 修改了認證規定,自至再也沒有所謂的終生有效的 LPIC 證照了。前幾天 LPI 又發佈消息指出,明年一月將會開始發動 LPI 的最頂級認證「LPIC-3」。LPIC-3 的認證也籌畫好幾年了,終於在今天看到這則另人感動的消息。
由於 LPIC-3 是針對大型企業應用與 IT 專家、顧問等級的認證考試,能取得 LPIC-3 這種級數的認證,會是一種技術能力上的肯定;不過,LPIC-3 必須具備「有效」的 LPIC-1 與 LPIC-2 才能報考,所以尚未取得前二級的認證,或是證照過期的朋友,可要先做一下準備功課了。
原文報導在此:http://www.linux-watch.com/news/NS8820915301.html
由本篇日記開始,我們將進行「Linux Device Driver 入門:I/O 處理」的議題討論。這裡所提的 I/O 處理定義是:user process 與 physical device 的 I/O 存取。
在讀「Linux Device Driver 入門:I/O 處理」專欄前,您必須熟悉 Linux 驅動程式的架構,因此「Linux Device Driver 入門:架構層」的專欄是 Jollen's Linux Device Driver 系列專欄的先備知識;此外,接下來的專欄使用的語法也必須對架構層有基本認識後才能看得懂。
Linux 驅動程式 I/O 機制
Linux device driver 處理 I/O 的「基本款」是:
-
fops->ioctl
-
fops->read
-
fops->write
另外「典藏款」則是 mmap,未來在「Linux Device Driver 進階」專欄裡再來討論這個主題。
fops->ioctl
ioctl 代表 input/output control 的意思,故名思義,ioctl system call 是用來控制 I/O 讀寫用的,並且是支援 user application 存取裝置的重要 system call。因此,在Linux驅動程式設計上,我們會實作ioctl system call以提供user application讀寫(input/output)裝置的功能。
依此觀念,回到架構篇所舉的 debug card 範例。當 user application 需要將數字顯示到 debug card 時,範例 debug card 0.1.0 便需要實作 ioctl system call,然後在 fops->ioctl 裡呼叫 outb() 將 user application 所指定的數字輸出至 I/O port 80H。
User application 使用 GNU LIBC 的 ioctl() 函數呼叫device driver所提供的命令來「控制」裝置,因此驅動程式必須實作 fops->ioctl 以提供「命令」給使用者。
fops->read & fops->write
read/write 是 Linux 驅動程式最重要的 2 個 driver function,也是驅動程式最核心的觀念所在。對驅動程式而言,read/write 的目的是在實作並支援 user application 的 read() 與 write() 函數;user application 是否能正常由硬體讀寫資料,完全掌握在驅動程式的 read/write ethod。
User application 呼叫 read()/write() 函數後,就會執行 fops->read 與 fops->write。read/write method 負責讀取使用者資料與進行裝置的I/O 存取。依照觸發資料傳輸的方式來區分,我們可以將 I/O 裝置分成以下 2 種(from hardware view):
- Polling:I/O裝置不具備中斷。
- Interrupt:I/O裝置以中斷觸發方式進行I/O。
根據I/O處理原理的不同(from software view),可以將 read/write method 的實作策略分成多種排列組合來討論。為了簡化討論內容,未來的日記將鎖定「Interrupt 式的 I/O」來做探討。
方才在 slashdot 上撇見「It looks like the newest version of the Linux kernel (2.6.20) will include KVM, the relatively new virtualization environment.」這篇 blog,kernel 2.6.20 已正式加入了 KVM 驅動程式,這也宣告 Linux 已經正式踏入「虛擬化(virtualization)」這個熱門的伺服器技術領域了。
Linux 的 kernel-space 虛擬機器驅動程式稱為「KVM」,KVM 於今年的 10 月 19 日首次正式發佈於 linux-kernel mailing list,並且於 11 月份時被 Linus 正式加入 kernel tree(根據此文)。這應該會是個有趣的好東西。
接下來,即將啟動 ELF 格式的「execution view」介紹。本日記是進入「dynamic loader」前的準備工作「之一」。
果然是很大挑戰的研究。說明幾個注意事項如下。
必須了解 ELF 的格式觀念
「Executable
and Linking Format」專欄已經把 ELF 格式的「linking view」做了初步的介紹,在此專欄裡,Jollen 針對 ELF
格式的概念以 loader v0.1~v0.5 共五個範例做說明。想要了解 ELF 格式的朋友,可以參考此專欄。
Section 的用途摘要說明
每個 section 都有不同的用途,在 SysV ABI 裡所定義的特殊 section 與其用途整理如下:
| Section |
用途 |
| .bss |
用來置放程式裡未初始化的資料(uninitialized
data),當程式執行時預設會將這裡的資料初始化為0(zero)。 |
| .comment |
置放版本控制資訊。 |
| .data |
置放已初始化的資料(initialized data) |
| .data1 |
置放已初始化的資料(initialized data) |
| .debug |
置放除錯時所使用資訊。 |
| .dynamic |
置放dynamic linking(動態連結)使用資訊。 |
| .dynstr |
置放dynamic linking所需的字串。 |
| .dynsym |
置放dynamic linking symbol table(動態連結符號表) |
| .fini |
當程式正常結束後,系統會執行此section裡的程式碼。 |
| .got |
Global Offset Table |
| .hash |
Symbol Hash Table |
| .init |
當程式開始執行時,系統會在進入程式進入點前(C的main() 函數)執行此section裡的程式碼。 |
| .interp |
置放program interpreter(程式直譯器)的path name(路徑名稱)。 |
| .line |
置放編譯後的機器碼與原始程式之間的對應資訊,利用gdb 的list命令可以列出object
file的原始程式碼,就是參考此 section的資訊。 |
| .note |
紀錄用section。 |
| .plt |
Procedure Linkage Table |
| .relname |
置放relocation資訊。 |
| .relaname |
置放relocation資訊。 |
| .rodata |
置放read-only(唯讀)資料。 |
| .rodata1 |
置放read-only(唯讀)資料。 |
| .shstrtab |
Section Name String
Table。置放section的名稱字串。請參考本章範例程式的說明。 |
| .strtab |
Symbol table的string table。 |
| .symtab |
Symbol Table |
| .text |
置放“text”資料,即可執行的程式指令。 |
前陣子「jserv」兄的深入淺出 Hello
World的演講把 ELF 格式「執行時期」的作業系統行為做了很不錯的「整體概念」呈現,對於執行時期 ELF object file 的概念呈現,jserv 兄的簡報會比我接下來要分享的專欄更為細膩而且連貫。
整體觀念:jserv 兄的「深入淺出 Hello World Part I/II」
很精采的 ELF 執行時期系統行為的分享。
雖然準備 ELF 與 loader 的專欄花了不少時間,但是還是覺得寫的不好。原因是,對於這種必須深入系統內部細節的技術討論,「整體概念」的一次呈現是相當重要的。對於 ELF 執行時期系統行為的討論,我要推薦 jserv 兄的「深入淺出 Hello World」系列演講;對於整體的概念呈現,jserv 的簡報做的比我的專欄還要好。
知識若能分享,就不必重造車輪,開放源碼分享的是知識,而不是程式碼。Jollen 自己的「ELF 執行時間」專欄,整體的呈現方式一直拿不定主意,還好,直接以 jserv 兄分享的簡報做為藍本,就容易多了!
需要了解 Linux 的 Memory Management 嗎?
需要懂一點概念,但不用深入 kernel code。
Linux 的 memory management 並不是容易撰寫的題目,這道題目的規劃走向可以是「kernel code 導向」,也可以是「概念式導向」,前者以 kernel code 來說明,後者以圖解觀念的方式進行;前者的優點是具體而且可以很細微,但是考驗作者的功力,後者的優點是概念能清楚呈現,但細膩度不足。
我認為,一開始選擇「概念式導向」的呈現方式會比較適當,這應該也是大家比較可以接受的方式。
.bss 節區
這是一個很特殊的節區,「linking
view」上,他不佔檔案空間、他用來存放「未初始化的變數」。Process(程式執行時)會具備這個特殊的節區。因此,我們只討論 .bss section
的「process virtual address」,而不討論「.bss section 所在的 file offset」。
之後再寫日記,以一段 code 來說明。
注記
- 2006.12.29: 調整標題。(Edited by Jollen)
.bss 節區存放「uninitialized data」,由程式碼的角度來看,就是「未初始化的變數」。我們直接以一段 code 來說明,讓大家更清楚這樣的概念。
#include <stdio.h>
int foo;
int bar;
int main(void)
{
int *ptr;
printf(".bss section starts at %08p\n", &foo);
printf("foo is %d.\n", foo);
ptr = &foo;
*ptr = 12345;
printf("foo is %d.\n", foo);
printf(".bss section starts at %08p\n", &foo);
return 0;
}
這段 code 相當簡單,但是隱含幾個重要的觀念,條列說明如下:
1. foo 是一個變數,在程式碼裡沒有被初始化(uninitialized),所以程式執行時(process),foo 變數會被擺在「.bss section」。
2. 同理,bar 變數也是。
3. foo 是第一個 uninitialized data,所以他的 virtual address,形同 .bss section 的開始位址(process virtual address)。
程式要實驗的項目如下:
1. 觀念 3. 的應用,我們印出 .bss section 的 start address。
2. foo 是全域變數,未初始化時的值是 0(zero)。
3. 用 '*ptr' 指向 .bss section 的 start address,此位址等於 foo 變數的值。
4. 把 .bss section 啟始位址處記憶體的值(value)改成 12345(透過 ptr 指標)。
沒搞錯的話,foo 變數的值就會變成 12345。
以下是執行結果:
# ./bss
.bss section starts at 0x8049588
foo is 0.
foo is 12345.
.bss section starts at 0x8049588
很特別的一個 section,值得深入研究。
在「理解 dynamic loader 內部原理的幾個先備知識(一)」講到:.bss 節區「linking view」上不佔檔案空間。這點可以用 readelf 來做 ELF linking view 端的印證:
# readelf -e bss|more (bss 是我們的範例執行檔)
...
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048108 000108 000020 00 A 0 0 4
[ 3] .hash HASH 08048128 000128 000028 04 A 4 0 4
[ 4] .dynsym DYNSYM 08048150 000150 000050 10 A 5 1 4
[ 5] .dynstr STRTAB 080481a0 0001a0 00004c 00 A 0 0 1
[ 6] .gnu.version VERSYM 080481ec 0001ec 00000a 02 A 4 0 2
[ 7] .gnu.version_r VERNEED 080481f8 0001f8 000020 00 A 5 1 4
[ 8] .rel.dyn REL 08048218 000218 000008 08 A 4 0 4
[ 9] .rel.plt REL 08048220 000220 000010 08 A 4 b 4
[10] .init PROGBITS 08048230 000230 000017 00 AX 0 0 4
[11] .plt PROGBITS 08048248 000248 000030 04 AX 0 0 4
[12] .text PROGBITS 08048278 000278 0001b8 00 AX 0 0 4
[13] .fini PROGBITS 08048430 000430 00001b 00 AX 0 0 4
[14] .rodata PROGBITS 0804844c 00044c 000031 00 A 0 0 4
[15] .eh_frame PROGBITS 08048480 000480 000004 00 A 0 0 4
[16] .data PROGBITS 08049484 000484 00000c 00 WA 0 0 4
[17] .dynamic DYNAMIC 08049490 000490 0000c8 08 WA 5 0 4
[18] .ctors PROGBITS 08049558 000558 000008 00 WA 0 0 4
[19] .dtors PROGBITS 08049560 000560 000008 00 WA 0 0 4
[20] .jcr PROGBITS 08049568 000568 000004 00 WA 0 0 4
[21] .got PROGBITS 0804956c 00056c 000018 04 WA 0 0 4
[22] .bss NOBITS 08049584 000584 00000c 00 WA 0 0 4
[23] .comment PROGBITS 00000000 000584 000132 00 0 0 1
...
重點的部份我用粗體字標示出來了:.bss section 與 .comment section 在檔案裡的 offset 是相同的。不過,用「他人」的工具來印可能會有一些盲點存在,比如說,我們可能不是很明白「Off」真正的意義;建議使用我們自行撰寫的 ELF 讀檔程式 loader-0.5.c(下載)來做,因為這是自己寫的工具,能保證一些盲點都能得到證明。以下是用 loader-0.5.c 印出來的畫面:
# ./loader bss
ELF Identification
Class: 32-bit objects
Machine: Intel 80386
Name Size FileOff
[00] .interp 19 244
[01] .note.ABI-tag 32 264
[02] .hash 40 296
[03] .dynsym 80 336
[04] .dynstr 76 416
[05] .gnu.version 10 492
[06] .gnu.version_r 32 504
[07] .rel.dyn 8 536
[08] .rel.plt 16 544
[09] .init 23 560
[10] .plt 48 584
[11] .text 440 632
[12] .fini 27 1072
[13] .rodata 49 1100
[14] .eh_frame 4 1152
[15] .data 12 1156
[16] .dynamic 200 1168
[17] .ctors 8 1368
[18] .dtors 8 1376
[19] .jcr 4 1384
[20] .got 24 1388
[21] .bss 12 1412
[22] .comment 306 1412
了解 ELF 並自己撰寫工具,此過程讓我們了解到「Offset」指的是「確實是該 section 在檔案裡的啟始讀取位置」。這代表,無論程式裡有多少 uninitialized data,都是不佔用額外的檔案空間的。
畫面中的節區大小
「Size」代表該 section 的實體大小(in bytes),以 .bss section 來說,.bss section 的大小是 12 bytes。很不幸的是,這個大小並非表示 .bss section 佔用的「檔案大小」,而是「記憶體大小」;這可能會是一個使用工具時,因為畫面的「字義」所不小心產生的盲點。所以如果把 .bss section 的 Offset 加上他的 Size,並不會等於 .comment section 的 Offset。
所謂的「Size」,包含由 objdump 與 readelf 所列印出來的畫面,或者說,「紀載在 section header entry」裡的 size 資訊,是表示「該 section 的實體記憶體大小」。
.bss section 的長度計算方式
.bss 的大小計算方式為(IA32 平臺):
4 bytes + sizeof(所有的 uninitialized data)
這代表 .bss section 在記憶體所會佔用的長度。以先前的例子來說,計算式會是:
4 + sizeof(foo) + sizeof(bar) = 4 + 4 + 4 = 12 (bytes)
所以,.bss section 的「size」field 就是 12。
.bss section 的結構
.bss section 的空間結構類似於 stack,所以前一則日記講述的「foo 是第一個 uninitialized data,所以他的 virtual address,形同 .bss section 的開始位址(process virtual address)。」觀念,並非全然正確。
此部份留待後續再做說明。
目前已經了解到:.bss section 在 linking view 時是不佔檔案長度的,在 execution view 時,根據其長度來佔用記憶體大小。
關於 .bss section 的結構,其實一張圖就夠了。直接切入重點吧!
前言
先重新編譯 bss.c 範例:
# gcc -g -o bss bss.c
# ./bss
.bss section starts at 0x8049588
foo is 0.
foo is 12345.
.bss section starts at 0x8049588
依照先前日記的說明,.bss section 的長度為 12 bytes。無論程式是否有 uninitialized data,process 一定會有
.bss section,並且 .bss section 的長度至少為 4 bytes(IA32),第一筆資料是 "completed.1",該筆資料紀錄 .bss
section 的起啟位址。
另外,在「linker script」裡,定義了一個叫 "__bss_start" 的符號,此符號才是紀錄 .bss section
的真正起始位址。不過,在此先不討論這個部份。
Process 的 .bss section 結構
Process 的 .bss section 佔用的記憶體大小,是根據 .bss section 的長度,在執行時期為每一筆 uninitialized
data 保留下來 |