« 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 保留下來的。其結構如下圖所示,我們以 bss.c 的範例來說明。
圖 .bss section 結構
由圖可以知道,範例的 .bss section 其 start address 為 0x8049584,這是直接查詢 .bss section 裡的
"completed.1" 符號得知的。'completed.1' 的 address 可利用 nm 查詢:
# nm -v bss|grep 'completed.1'
08049584 b completed.1
由此了解,.bss section 真正的 start address 應該是 'completed.1';不過,若把第一筆資料的 start address 當做 .bss section 的 start address 其實也無妨,或者說這是 .bss section「放 data」的 start address。
利用 gdb 來觀察
# gdb ./bss
...
(gdb) disassemble 0x8049588
Dump of assembler code for function foo:
0x08049588 <foo+0>: add %al,(%eax)
0x0804958a
<foo+2>: add %al,(%eax)
End of assembler dump.
(gdb) disassemble 0x8049584
Dump of assembler code for function completed.1:
0x08049584 <completed.1+0>: add %al,(%eax)
0x08049586 <completed.1+2>: add %al,(%eax)
(gdb) disassemble 0x804958c
Dump of assembler code for function bar:
0x0804958c <bar+0>: add %al,(%eax)
0x0804958e <bar+2>: add %al,(%eax)
End of assembler dump.
Linus 本人在 Linux 2.6 時代所提出的 "initramfs" 是一種更好的 "root=" 做法。在這裡先不對 initramfs 做詳細介紹與原由說明,不過,簡單來說,initramfs 就是「kernel 2.6 的 initrd」。Initrd(initial ramdisk)即 /dev/ram0,是一種普遍使用在 embedded Linux 的觀念;embedded Linux 利用 initrd 來載入 root filesystem(或是 bootstrap root filesystem 後再以 NFS 掛進完整 RFS)。
本日記介紹製作 Linux 2.6 的 initramfs 簡易方法。假設 RFS 的目錄為 /home/rootfs,則製作 initramfs 的指令為:
# cd /home/rootfs
# find . | cpio -o -H newc | gzip -9 >../cramfs.gz
更明確地說,initramfs 是 "compressed" ramfs(ram filesystem),ramfs 並不等於傳統的 ramdisk;像是以 "genext2fs" 工具所製作的 initrd(ext2 格式的 image file)才是基於 ramdisk 的傳統做法,也就是說,initramfs 並不等於傳統上的 initrd,這點要請大家多留意。
本文只適合「Embedded Linux / x86: 基礎能力與系統管理」的學員、對 root filesystem 建立有基本概念的朋友,或是「Embedded Linux 嵌入式系統實作演練, 2e」的讀者。
本日記介紹更方便的 nano-X 專案建立方式,以下做法使用的 shell script 並「不是」Jollen 原創,而是 Jollen 的恩師「Joe」貼心提供給學員使用的。目前 Joe 老師在大學主持嵌入式 Linux 實驗室,上過 Joe 老師課的同學仍然可以寫 email 跟他請教問題。
步驟
1. 利用 gennanox 來產生主要的目錄(骨幹):
# sh ./gennanox
generating fstab
generating group
generating passwd
generating inittab
generating rc.d/rc.init
generating rc.d/rc.sysinit
generating /bin/microwin
2. 得到 nanox/ 目錄:
# ls -l nanox/
總計 8
drwxr-xr-x 4 root root 4096 12月 19 18:55 i386.so
drwxr-xr-x 2 root root 4096 12月 19 18:55 src
# ls -l nanox/i386.so/
總計 8
drwxr-xr-x 12 root root 4096 12月 19 18:55 install
drwxr-xr-x 2 root root 4096 12月 19 18:55 pub
nanox/install/ 存放我們的 root filesystem。
3. 把 package source tarball 都放到 nanox/src/,build 好的 binary 放到 nanox/i386.so/install/。
4. root filesystem 的 dev/ 不用再手動建立檔案。有一種更方便的方法是寫一個 device txt 檔,透過 genext2fs 來做,把下面的內容存成 dev.txt:
# name type mode uid gid major minor start inc count
/dev d 755 0 0 - - - - -
/dev/console c 666 0 0 5 1 0 0 -
/dev/fb c 666 0 0 29 0 0 1 1
/dev/mouse c 666 0 0 10 1 0 0 -
/dev/ram b 666 0 0 1 0 0 1 4
/dev/tty c 666 0 0 5 0 0 0 -
/dev/tty c 666 0 0 4 0 0 1 2
/dev/gpmdata p 666 0 0 - - - - -
/dev/null c 666 0 0 1 3 0 0 -
/dev/zero c 666 0 0 1 5 0 0 -
/dev/hda b 666 0 0 3 0 0 0 -
/dev/hda b 666 0 0 3 1 1 1 9
/dev/hdc b 666 0 0 22 0 0 0 -
/dev/hdc b 666 0 0 22 1 1 1 9
# usb mouse
#/dev/input/mice c 666 0 0 13 63 0 0 -
#/dev/mouse c 666 0 0 13 63 0 0 -
/dev/input d 755 0 0 - - - - -
/dev/input/mouse c 666 0 0 13 32 0 1 2
/dev/input/event c 666 0 0 13 64 0 1 4
5. 打包 RFS 時,透過 genext2fs 的 '-D' 參數來建立 device file。指令如下:
# cd nanox/i386.so/
# genext2fs -D dev2.txt -d install/ -b 2048 pub/ext2fs
# gzip -9 pub/ext2fs
#
把 RFS(install/)打包成 image file 時,genext2fs 會根據 dev.txt 的設定來建立 device file。打包好的 image file 要放到 nanox/i386.so/pub/ 目錄下,以便能系統性的管理整個 project。
以上的 dev.txt 同樣由 Joe 老師提供,他也把 USB mouse 的 device file 設定好了。gennanox 可以說是「用來產生 nano-X project 基本骨架(framework)」的 script。
Thanks Joe!
有時候換個心境來「考古」一下,比學習新技術更有樂趣。
The Unix Heritage Society
「The Unix Heritage Society(tuhs)」保留了很久以前的 UNIX 系統,引述 tuhs 網站上的一句話:
The preservation and maintenance of historical and non-mainstream UNIX systems.
tuhs 也提供 rsync 服務,所以我就把上面的檔案都抓回來了!根據 tuhs 網站上的說明,我下的 rsync 命令如下:
rsync -avz minnie.tuhs.org::UA_Root .
rsync -avz minnie.tuhs.org::UA_Applications Applications
rsync -avz minnie.tuhs.org::UA_4BSD 4BSD
rsync -avz minnie.tuhs.org::UA_PDP11 PDP-11
rsync -avz minnie.tuhs.org::UA_PDP11_Trees PDP-11
rsync -avz minnie.tuhs.org::UA_VAX VAX
rsync -avz minnie.tuhs.org::UA_Other Other
抓了好久,因為檔案差不多有 1.1 GB 左右。抓回這些東西:
jollen@jollen-work:~/tuhs$ ls
4BSD/ checksums.gz lists/ Other/ rsync.sh VAX/
Applications/ COPYRIGHTS misc/ PDP-11/ tools/
Caldera-license.pdf Documentation/ nohup.out README updates
又找了 DEC PDP-11 的模擬器,想來玩玩看這些古老級的 Unix。
The DEC Emulation Website
在這裡可以找到 DEC 的模擬器:http://www.aracnet.com/~healyzh/decemu.html,DEC PDP-11 的模擬器:http://www.aracnet.com/~healyzh/pdp11emu.html。不過還沒有實際裝起來玩過,只是覺得蠻酷的。
Bob Supnik's PDP simulator
從 tuhs 抓回來的東西裡找到可以在 Bob Supnik's simulator 上跑的 211BSD:
jollen@jollen-work:~/tuhs$ ls PDP-11/Boot_Images/2.11_on_rl02/
211bsd.boot.gz 211bsd.vol3.gz 211bsd.vol6.gz 211bsd.vol9.gz README
211bsd.vol1.gz 211bsd.vol4.gz 211bsd.vol7.gz patches script
211bsd.vol2.gz 211bsd.vol5.gz 211bsd.vol8.gz pdpsim.tz
Bob Supnik's PDP simulator 首頁:http://simh.trailing-edge.com/,裡頭有好多古董機的模擬器,PDP-11 模擬器的功能列表:http://simh.trailing-edge.com/pdp11.html。
為了能看到 2.11BSD on PDP-11 跑起來的樣子,所以要找一個簡單的方法先讓東西跑起來。由 tuhs 上 mirror
回來的一堆東西裡,找到 PDP-11/Boot_Images/2.11_on_Simh/211bsd.tar.gz
這個檔案,直接取用這個檔案,把它解開後發現裡頭有這麼多東西:
這裡頭的檔案是給 SIMH 模擬器用的,也就是「Bob
Supnik's PDP simulator」。我用的是 SIMH V3.6-1,以下是編譯後得到的一堆古董機模擬器:
我把 211bsd.tar.gz 解在這個目錄下。接著用 PDP-11 的模擬器來開機:
$ ../pdp11 ./211bsd.simh
結果跑出這個畫面:
一時之間不曉得發生什麼事,結果按了個 [Enter] 後:
接下來,大家應該會做跟我一樣的動作:
還蠻酷的。
I/O 存取相關函數
要提到「I/O 處理」當然要整理 Linux 提供的相關函數,以下分 3 大類來整理:
1. I/O port
2. I/O memory
3. PCI configuration space
4. ioremap
I/O Port
以下是 Linux 提供最原始的 I/O port 存取函數:
˙ unsigned inb(unsigned port);
˙ unsigned inw(unsigned port);
˙ unsigned inl(unsigned port);
˙ void outb(unsigned char byte, unsigned port);
˙ void outw(unsigned short word, unsigned port);
˙ void outl(unsigned long word, unsigned port);
I/O Memory
以下是 Linux 提供最原始的 I/O memory 存取函數:
˙ unsigned readb(unsigned port);
˙ unsigned readw(unsigned port);
˙ unsigned readl(unsigned port);
˙ void writeb(unsigned char byte, unsigned port);
˙ void writew(unsigned short word, unsigned port);
˙ void writel(unsigned long word, unsigned port);
對於 I/O memory 的操作,Linux 也提供 memory copy 系列函數如下:
˙ memset_io(address, value, count);
˙ memcpy_fromio(dest, source, num);
˙ memcpy_toio(dest, source, num);
以上在「Linux 驅動程式觀念解析, #6: 依流程來實作 -- Physical Device Driver」介紹過一次,並且也搭配了一個簡單範例做說明,您可參考該文。
PCI Configuration Space
Linux 也提供讀寫 PCI configuration space(PCI BIOS)的函數:
˙ int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val);
˙ int pci_read_config_word(struct pci_dev *dev, int where, u16 *val);
˙ int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);
˙ int pci_write_config_byte(struct pci_dev *dev, int where, u8 val);
˙ int pci_write_config_word(struct pci_dev *dev, int where, u16 val);
˙ int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);
有些朋友可能看過開頭是 pcibios_* 的函數版本,「不過這是舊的函數,請勿再使用」。
ioremap()
這個 API 就重要到不行了,任何時候,Linux device driver 都「不能直接存取 physical address」。所以,「使用以上的 I/O 相關函數時,只能傳 virtual address,不能傳入 physical address」,ioremap() 就是用來將 physical address 對應到 virtual address 的 API。
小結
對於 I/O 函數的使用,應該在「深諳」Linux驅動程式架構與Linux作業系統原理的情況下使用,「單純的 kernel module + IO APIs」並不叫做 Linux 驅動程式,再更進一步的「kernel module + read/write/ioctl + IO APIs」也只是小聰明(編註),還是稱不上 Linux「驅動程式」。建構在作業系統裡的驅動程式,90% 都是在實作良好的機制與行為,因此「OS 原理與機制的研究」,才是正確的思考方向。與大家分享自己的心得,希望對您的學習有幫助。
編註:這是 Linux device driver 的「開始」但不是全部,也只是冰山一角。但是許多教育訓練機構的課程卻是以此為做為規劃方向,並不是很妥當。
SDIO 是走 SD 插糟的週邊介面,SDIO 也是普及於 PDA/Mobile 的週邊規格,實際應用例如:透過 SDIO(SD 插糟),可以外接許多 SDIO 的週邊卡,像是 SDIO WiFi card 或是 SDIO CMOS sensor。Linux 從 2.6.17 開始正式加入了 SD/MMC 的 stack driver(也就是 API core 層),但是並沒有 SDIO 的 stack driver。
因此,「玩家」可以試著由現有的 SD/MMC stack 做修改,並實作 machine-dependent 的驅動程式(例如:s3c24x0)。或是花錢買解決方案,例如:MontaVista、印度的 embWise 公司。
Linux-SDIO Project
不過,有好消息。今天在 LinuxDevices.com 上看到一則新聞:MontaVista,
Atheros contribute open-source SDIO stack。MontaVista 與 Atheros 攜手貢獻
open-source 的 SDIO stack,並且未來也會加到主流(mainline)的 Linux kernel(註:即 Linus 維護的
kernel)。這個動作解決目前幾個 SD/MMC 應用的問題:
1. Linux SDIO stack(Core API layer)終於有了完整的 open-source
版本。(但是不知道實際採用的意願高不高,不過對這個部份有興趣的玩家,終於不用再面對一疊厚厚的 spec. 發呆了。)
2. Linux 未來將能支援更多的 SDIO 週邊。目前除了 SD memory card、SDIO WiFi card 與
SDIO CMOS sensor 可以跑之外,不知道還有什麼 SDIO 週邊可以上到 Linux 的。
由於 Atheros 有 Linux 2.6 的 SDIO WiFi card patch,所以除了 Atheros
的卡可以上之外,好像也沒辦法在「網路上」找到別家的。
這裡有 [MontaVista
所維護的 Atheros WiFI card SDIO 驅動程式]。
3. MontaVista 與 Atheros 合作的這個 open-source 專案,做了一件很重要的事(引述報導原文):
The linux-SDIO project
will apparently endeavor to merge the Codetelligence/Atheros stack with the
mainline kernel's existing linux-mmc subsystem.
此外(引述報導原文):
our long term goal is to follow the community and just submit
SDIO drivers to Pierre [Ossman, Linux's MMC subsystem maintainer], instead of
duplicating the SDIO framework of host controller drivers and bus drivers.
所以 Linux-SDIO project 將會整合現有的 Linux-MMC subsystem(即 Stack
層、API)。表示未來 Linux 的 SD/MMC subsystem 只會有一套,而不是像現在的情況。例如:有些 MMC host controller 的
vendor 除了實作自己的 machine-dependent driver 外,也寫自己的 stack 層(只是沒有那麼完整、通用)。
4. Open-source 的價值之一是「製造出」各種完善的商業性支援(commerical support),所以許多工作並不需要自己來,或是自己做不來的工作也能輕鬆取得支援。以 SDIO
stack 解決方案來說,未來將會有更多 IDH(Independent Design House)提供 SDIO 的協助,商業性的支援不會再是有錢人的專利了。
目前 Linux 2.6 現有的 SD/MMC stack
架構與程式碼實作,難度並不是很高,相當好理解,大家可以花點時間來研究這個部份,會很有幫助的。
Open-source SDIO stack,這真是個不錯的消息!
non-GPL 授權的 Linux kernel module 是否在 2008 年的一月份起開始禁止!?
「SEJeff」在 OSNews.com 上寫了一篇 Blog,內容提到「non-GPL Linux kernel modules will not work after January 2008」,結果更可怕的是截至目前(本日記出版時)為止,已經有 27x 篇的「comments」在討論這個話題。
Comments 裡頭各式各樣的延伸看法都有,大家可以去瞧瞧。
Merry Christmas
and Happy New Year 2007!
祝 jollen.org
的所有朋友耶誕佳節愉快。
--jollen
|
|
|
|
|
本年度與明年度的既定工作之一是「survey of application framework for Linux mobile phones」以及「what are they target」。今天是歡樂的 Christmas,不過是要工作的一天;去年與前年的耶誕節正好遇到假日都是放假。但是也有許多公司今天很人性化的放了一天假,結果就在閒逛網站時,又瞄到一則新聞「Framework aims to commercialize mobile Linux apps」。
如果我的國文造詣還可以的話,以「雨後春筍」來形容「Linux application framework for mobile devices」應該是挺合適的;今年(2006)如果要寫 Linux 年度大事記的話,這個主題絕對是前 10 名的。
本週是2006年最後一週,也是為2007年做準備的最後一週,輕鬆一下也是不錯的。
回歸正題
另一個以 GTK+ 為基礎的 Linux mobile devices 的「application framework」也釋出了,這個 application framework 叫做「Hiker Application Framework」,他是以 GTK+ 做為發展基礎的 Linux mobile devices 應用框架。
不久前曾經提到過的「OpenMoko」也是以 GTK+ 為基礎的 Linux mobile devices 應用框架(application framework);OpenMoko 與 Hiker Application Framework 在授權上採取不同的策略。OpenMoko 以 GPL(but not as usual mentioned by OpenMoko's web site),Hiker Application Framework 則是以 MPL(Mozilla Public License)的授權釋出。
Hiker Application Framework 的資訊可參閱他的官方網站「Access」,並且可在此下載。
Hiker 於 2006 年 10 月份曾經發出過一份消息,可按此瀏覽。另外,Access 預計在 2007 年上半年釋出 SDK(software development kit)與 PDK(product development kit),值得一提的是,SDK 所提供的 GUI 工具是「Eclipse-based 」。
LinuxDevices.com 推出「Embedded Linux 2006 十大回顧」!內容提到今年 10 個主要的 embedded Linux 發展趨勢(以下是簡要翻譯,請閱讀原文有很完整的說明):
1. Linux 也有自己的手機囉!
2. 許多廠商開始提供完整("complete")的 mobile phone software stacks 了,參賽者有 Trolltech, a la Mobile, Cellunite, Aplix, Access!(Jollen 編註:事實上 2006 年出現在地球上的 mobile phone software stack(application framework)不只於此)
3. 關鍵性的 embedded Linux 開發工具!(Jollen 編註:最令人印像深刻的是 TimeSys 的「現成 RFS 服務」」)
4. AMD & Intel 賣掉他們「non-x86」的 mobile phone 處理器產品線!(Jollen 編註:你們這哥倆好要繼續努力喔!)
5. 真的不是蓋的,越來越多 Eclipse-based 的 embedded Linux 開發工具了!(Jollen 編註:其中以「MontaVista 推出 Dev Rock 5」是最重大的變革)
6. GPLv3 大激戰!
7. 除了手機外,還有更多的 design wins!
8. VoIP、TV 與 Set-top-box 的產品!
9. Linux kernel 有了重大的 real-time enhancements!
10. 許多 Linux 新規格的釋出!
今年隨時保持 embedded Linux 發展動態的朋友,對這幾條新聞的出線絕對是不感到意外的。不容易被外力改變的方向叫做「趨勢」,希望未來的一年 embedded Linux 繼續加油!
重要觀念
任何作業系統底下的「驅動程式」,都需要分二個層面來討論所謂的「I/O 處理」:
1. 實體層:驅動程式 v.s. 硬體。
2. 虛擬層:驅動程式 v.s. user process
在前一篇日記「Linux
驅動程式的 I/O, #2: I/O 存取相關函數」中所提到的 I/O 函數是處理「實體層」的 I/O;本日記所要介紹的 copy_to_user()
與 copy_from_user() 則是在處理「虛擬層」的 I/O。另外,在繼續往下讀之前,您必須了解以下的觀念都是「等價」的:
1. 驅動程式與 user process 的 I/O;等於
2. 驅動程式與 user process 間的 data communication;等於
3. kernel-space 與 user-space 間的 data communication。
此外,還要了解:
1. user-space 無法「直接」存取 kernel-space 的記憶體。
2. 「Linux device driver」與「user-space」間的 I/O 會與 fops->read、fops->write
與 fops->ioctl 共三個 system call 有關。
copy_to_user() 與 copy_from_user()
了解以上的觀念後,再來「直接殺進重點」就很容易懂了:從 user-space 讀取資料至 kernel-space,或是將 kernel-space
的資料寫至 user-space,「必須」透過 kernel 提供的 2 個 API 來進行。這二個 API 如下:
˙ long copy_to_user(void *to, const void *from, long n);
˙ long copy_from_user(void *to, const void *from, long n);
參數說明,以 copy_to_user() 來說:
˙ to:資料的目的位址,此參數為一個指向 user-space 記憶體的指標。
˙ from:資料的來源位址,此參數為一個指向 kernel-space 記憶體的指標。
˙ 口訣:copy data to user-space from
kernel-space
以 copy_from_user() 來說:
˙ to:資料的目的位址,此參數為一個指向 kernel-space 記憶體的指標。
˙ from:資料的來源位址,此參數為一個指向 user-space 記憶體的指標。
˙ 口訣:copy data from user-space to
kernel-space
由 user-space 讀取資料,或是寫入資料給 user-space 的 3 個 driver method 為:read、write與ioctl。
另外,指向
user-space 的指標是 kernel 回呼 driver method 時所傳遞進來的,可由 read、write 與 ioctl driver
function 的函數原型宣告來觀察(紅色部份):
˙ int card_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
unsigned long arg);
˙ ssize_t write(struct file *filp, const char *buff, size_t count, loff_t *offp);
˙ ssize_t read(struct file *filp, char *buff, size_t count, loff_t *offp);
fops->ioctl 的參數 arg、fops->write 與 fops->read 的參數 buff
是指向 user-space 資料的指標。撰寫程式時,要注意資料型別上的不同。
下一篇日記再寫一個範例來配合著研究,大家應該會更清楚。
(非關 Embedded Linux)今天晚上跟小弟弟提到「Google Trends」可以用來幫助我們了解「關鍵字的搜尋趨勢」,結果丁小弟寄了以下二張圖片給我。
好一個「罄竹難書」。
uClinux 的 distribution 稱為 uClinux-dist,今天把 uClinux-dist 使用的教學專欄做了小小修正與整理,並提供給有需要的朋友。此系列教學專欄共有 5 篇文章,如下:
2004.02.26: uClinux-dist 使用教學, #1:uClinux 與 uClinux-dist 介紹
2004.02.26: uClinux-dist 使用教學, #2:安裝 uClinux 的 GNU ARM toolchain
2004.02.26: uClinux-dist 使用教學, #3:uClinux-dist 的使用
2004.02.26: uClinux-dist 使用教學, #4:編譯 uClinux-dist 與常見問題
2004.02.26: uClinux-dist 使用教學, #5:使用 GDB/ARMulator 來做測試
專欄的目錄索引「http://www.jollen.org/EmbeddedLinux/uClinux-dist.html」,需要時歡迎隨時上線取用。
前些日子寫到「理解 dynamic loader 內部原理的幾個先備知識(上)」時提及,必須略懂 .bss section 的觀念,並且最後也提到「之後再寫日記,以一段 code 來說明。」。其實之後幾天我就把介紹 .bss section 觀念的日記寫好了,只是還沒有加入到專欄索引,趕快來把索引做好!
今天加入「BSS Section Concepts: .bss section 的基本觀念介紹」的專欄索引。
以下是必讀的二個專欄(及理由):
1. 必須先懂 ELF 到底是什麼東西,才會知道 .bss 是 ELF 的「一個節區」。可以參考 Jollen 的「 Executable and Linking Format: 重要的 ELF 格式介紹」專欄。
2. 接著才是讀「BSS Section Concepts: .bss section 的基本觀念介紹」專欄。.bss section 是「動態時期(run time)」,而不是「靜態時期(stored)」的觀念,這真的是很重要。
除了「理解 dynamic loader 內部原理的幾個先備知識(上)」提到以 ELF 為主軸的幾個重要先備知識外,在開始研究 dynamic loader 前,還有幾個重要觀念也是要先懂的。其中與 kernel 相關的議題整理如下:
1. 就整體來看,最好了解 kernel 的開機流程,也就是由 bootloader 載入 kernel 後一直到 init process(user-space)的整個過程。這部份,Jollen 正在準備一篇簡單的文章來跟大家分享。
2. 就內部來看,必須知道「下指令後、程式怎麼載入與執行」的流程。大致來看,這涉及 shell、fork system call、exec system call 三大主題。這部份,Jollen 也打算寫篇日記來介紹。
了解以上二個重要觀念後,再切入研究 dynamic loader(例如:process 與 libc.so.6 如何 "dynamic" linking)時,腦細胞的連結才會比較順暢。
本系列專欄是「了解 ELF 動態時期行為」的前導專欄,建議您先行爬文「理解 dynamic loader 內部原理的幾個先備知識(下):Kernel 端的議題」。
Shell 執行外部程式的觀念
在 2006 年的最後一天,就先來解說一下「下指令後、程式怎麼載入與執行」的觀念。首先,您必須知道並具備以下的 Linux 系統程式觀念:
1. fork() 系統呼叫:用來建立 child process。
2. exec 系列系統呼叫:以一個「外部程式」來取代自己(current process)的執行空間。
3. parent process、child process 與 Linux process tree。
了解以上三個主題後,就可以很容易理解「下指令後、程式怎麼載入與執行」的過程;這是很重要且基本的三個議題,若您不是很清楚這三個議題的觀念,可以查詢「Linux Systems Programming」的相關文件。
接下來,首先我們要知道的是,Linux 並沒有提供 "spawn" 的 system call;再來,shell 執行外部程式的做法是使用「spawn 版」的 fork() 實作。
好像又把大家搞胡塗了。所謂的「spawn 版的 fork()」,用對照的方式來說明會很清楚:
1. 真正的 spawn() 是「建立一個外部程式的 child process」;
2. 而 Linux 下討論的 fork() 則是「建立一個與父程序(parent process)完全相同的 process」。
也就是說,Linux 下的 fork() 是「non-spawn behavior」,因為他不會去跑一個外部程式,而是「複製 parent process 成為 child process」;那麼,要怎麼做出「spawn behavior」的 fork() 呢?方法很簡單:
1. 我們已經學過, exec 系列系統呼叫會以一個「外部程式」來取代 current process 的執行空間,所以不能在 parent process 裡叫用 exec system call 來跑外部程式;否則 parent process 會消失。
2. fork() 可以建立 child process,並且不讓 parent process 消失。
3. 所以,如果先 fork() 出 child process,再讓外部程式取代 child process,那麼就可以實作出「spawn style 的 fork()」了。
小結
Jollen 一開始就打算以邏輯思考(理論推演)的方式來介紹「觀念」,所以講了這麼多,希望不會造成您閱讀上的困擾。引用「書上寫的」應可以勝過以上的長篇大論:
Shell 執行外部程式的做法是先 fork 出自己的 child process,然後在 child process 裡使用 exec 系統呼叫外部程式,以執行外部程式。
很簡單的概念,只是記結論的話多少會少掉一些思考的樂趣。依此來看,邏輯上(Linux programming view)「可視 spawn 為 fork + exec」;實際上,若由 kernel view 來解讀,spawn 並不等於 fork + exec。
程式範例
看此範例需要知道 fork() 與 exec 系列函數的用法,您可參考「Linux Systems Programming」相關文件。
/*
* Copyright(c) 2003,2004,2005,2006 www.jollen.org
*
* Spawn-style Linux fork().
*
* This file is GPLed.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int spawn(char *prog, char **arg_list)
{
pid_t child;
child = fork();
if (child != 0) {
return child;
} else {
execvp(prog, arg_list);
fprintf(stderr, "spawn error\n");
return -1;
}
}
int main()
{
char *arg_list[] = {
"ls",
"-l",
"/tmp",
NULL };
spawn("ls", arg_list);
return 0;
}
以上範例可由「http://tw.jollen.org/elf-programming/spawn_fork.c」下載。
Happy New Year ;-)
|