|
|
11月24日 http://www.play-hookey.com/
11月18日 .bss section 的觀念:uninitialized data section
jollen 發表於 December 15, 2006 1:11 AM
.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,值得深入研究。
--jollen .bss section 的觀念:執行時期的長度
jollen 發表於 December 15, 2006 2:02 AM
在「理解 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)。」觀念,並非全然正確。
此部份留待後續再做說明。
--jollen bss section 的觀念:執行時期的結構說明
jollen 發表於 December 17, 2006 10:02 PM
目前已經了解到:.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.
--jollen
http://library.kingofcoders.com/index.php?topic=linux http://www.jollen.org/EmbeddedLinux/
http://blog.linux.org.tw/~jserv/archives/002030.html
窺探 .bss section
幾年前只是對系統設計感到困惑,沒想到「 分析 GCC 對 Hello World 的重重布幕」一類的舉動,竟成為激勵自我成長的目標,實在始料未及。拜 C 語言這種「披著高階語言羊皮的低階語言之狼」所賜,我們可透過稍早 blog [ 自我印列 ELF 簽名]
所提及的途徑,探索記憶體位址背後的意義。同樣地,我們也可從實驗觀察 GNU/Linux 中 ELF (executable and
linkable format) 格式執行檔裡頭 .bss section 的呈現,關於這部份的背景知識,可參閱 Jollen 整理的 [ .bss section:C 語言所種下的因] 與 [ BSS Section 觀念教學] 等文章,本文則針對「窺探」的手法作補充。
「窺探」ELF 執行檔有許多途徑,我們當然可用 binutils 裡面的 readelf / objdump 工具,但這裡我們直接用程式自我列印,筆者給定的程式如下:
#include <stdio.h>
extern int __bss_start, _end;
int a, b, c, d; /* un-initialized */
int main() { int *ptr; a = 1, b = 2, c = 3, d = 4;
for (ptr = &__bss_start; ptr != &_end; ptr++) { printf("%d\n", *ptr); } return 0; }
為行文便利,此小程式命名為 [ bss.c],咱們就先試著執行看看。在筆者的電腦安裝有 gcc 3.4 與 4.3 兩個版本,先用 gcc-3.4 看看: $ gcc-3.4 -xc bss.c && ./a.out 0 4 1 2 3
由上可見,C 語言程式碼中的 int a, b, c, d 在宣告的時候,並未給定數值,也就是「未作初始化」,這樣的變數在 ELF
的角度來看,就存放於 .bss section,而在 main() 中,這四個變數都在執行時期 (runtime)
被給定數值,上述的程式透過迴圈,將給定的 1, 2, 3, 4 等值都印列出來
(儘管順序不是預期的遞增排列,不過本文不會深入分析),這是怎麼做到的呢?關鍵之處就在於一開始宣告的這行:
extern int __bss_start, _end;
注意到此行前方的 "extern" 關鍵字,在 GNU Toolchain 會對名稱為 "__bss_start"
與 "_end" 的符號作特別處理,在預設的 linker script 中,會給定輸出 ELF 執行檔的 .bss section
的資訊,重點是,經過這樣的操作後,"__bss_start" 與 "_end" 只是 label
而非真正的變數,所以,並不佔用真正的記憶體空間。在 C 語言中,我們可取得其位址作指標的尋訪過程,以逐一得知 .bss section
各元素的內容值,這下似乎明暸了,但回顧剛剛的執行輸出,我們不免對其中的 "0" 感到困惑,是啊,這值到底從哪邊來?
在找尋答案之前,筆者改用 gcc-4.3 來作測試,其執行輸出如下: $ gcc-4.3 -xc bss.c && ./a.out 0 0 4 1 2 3
感覺起來就更離奇了,「又」多了一個 "0" 的輸出?!看來是 GNU Toolchain 對 ELF 執行檔額外施加了「魔法」,看來得搬出其他工具來分析。先觀察 objdump 對 .bss section 的分析: $ gcc-3.4 -xc bss.c && objdump --section=.bss -x a.out ... SYMBOL TABLE: 08049598 l d .bss 00000000 .bss 08049598 l O .bss 00000001 completed.1 0804959c g O .bss 00000004 d 080495a0 g O .bss 00000004 a 080495a4 g O .bss 00000004 b 080495a8 g O .bss 00000004 c
可以發現,事實上程式碼的 &__bss_start 勢必指向 ELF 執行檔透過 Program Loader 映射到記憶體中的
BSS 區域,而我們在程式中尋訪 .bss section 中的元素,大抵就是依照上面的 SYMBOL TABLE
的排列方式,而之前那個印列出的 "0" 數值,就是 "completed.1" 這個符號的內含值。同理,我們觀察透過 gcc-4.3
編譯時的分析結果:
$ gcc-4.3 -xc bss.c && objdump --section=.bss -x a.out ... SYMBOL TABLE: 0804a014 l d .bss 00000000 .bss 0804a014 l O .bss 00000001 completed.6625 0804a018 l O .bss 00000004 dtor_idx.6627 0804a01c g O .bss 00000004 d 0804a020 g O .bss 00000004 a 0804a024 g O .bss 00000004 b 0804a028 g O .bss 00000004 c
在這份輸出中,我們看到形似剛剛 "completed.1" 的 "completed.6625" 符號,也多了名稱為
"dtor_idx.6627" 的符號。為了揭開謎團的真相,筆者又用 gcc-4.1 與 gcc-4.2 作實驗,這兩者得到與 gcc-3.4
編譯時相仿的輸出,但 "completed." 符號後方的數值名稱是不一樣的,由此可歸納,gcc-4.3 引入了一些我們未察覺的修改,而在
gcc-3.4 到 gcc-4.2 之間的 GNU Toolchain 所編譯的 ELF 執行檔,其 .bss section
也隱含我們不甚明暸的細節。
未完,待續
由 jserv 發表於 June 19, 2008 05:33 PM
非常好奇为什么“順序不是預期的遞增排列”,请指教,谢谢!
由 Wei 發表於 June 20, 2008 02:10 AM
我猜順序是因為 parse tree 之 traverse 的關係?
由 c9s 發表於 June 22, 2008 01:59 AM
11月15日 今天突然有个网友加我QQ,说他正在用gprof分析一个项目的源代码,想打印出出该项目的函数调用关系图,不料它参考的资料[1]中用于打印函数调用关系图的Mkgraph脚本已经无法下载了,所以想问我要一份。可我当初看到这篇资料时也因为也没下到,所以给它推荐了另外一个同样能够产生函数调用关系图的工具——calltree。不过刚才又突然想起,其实还有另外一个工具可以代替Mkgraph的,那就是kprof。为了方便大家日后分析源代码,这里一并再把几个相关工具介绍一下。
先列出我当前用的这几个工具的版本和它们的下载地址:
calltree 2.3 下载地点 http://linux.softpedia.com/progDownload/calltree-Download-971.html gprof 2.18.0.20080103 在ubuntu/debian下直接安装即可 http://citeseer.ist.psu.edu/graham82gprof.html kprof 1.4.3 (在ubuntu/debian下直接用apt-get安装) http://kprof.sourceforge.net/ graphviz (在ubuntu/debian下直接用apt-get安装即可,需要它的一个dot工具) http://www.graphviz.org/
1. introduction
对于一个C语言编写的项目,它的框架可以反应为一棵函数调用树。如果在分析项目之前,能够得到这样一颗调用树,那么就可以了解项目的整体框架;如果在项目运行之后,能够跟踪到该次运行过程中的函数调用,那么将有利于分析某些测试条件下项目的执行流程;而如果在项目运行过程中(比如调试项目时)能够跟踪出某个位置之前的函数调用,那么将有利于确定潜在bug可能存在的位置。
对于这三种情况,虽然没有任何一个工具能够完全满足,不过"聪明"和"乐于奉献"的程序员们还是分别贡献了不同的工具:
无须运行项目本身,calltree就能够根据整个项目的源代码产生一棵函数调用树,并可把该调用树导出为dot格式的图形。因此可以说calltree能够在不运行项目的条件下对项目进行函数级别的分析。 gprof则能够在项目运行之后,把该次运行过程中的函数调用以文本的形式反应出来,不过善于思考的人们总是喜欢更美好的生活,于是kprof产生了,它不仅可以辅助gprof更好的分析程序代码级别的运行情况,而且能够导出当前执行过程中的函数调用树,并同样可以把调用树导出为dot格式的图形。
gdb(Gnu DeBugger),这个应该很熟悉吧,它是一个调试工具。它提供专门的backtrace命令来跟踪程序执行到某个位置(比如指定的断点处)之前的函数调用。不过这个目前还是文本输出的,感兴趣的可以hack一下gdb,给它加上漂亮的输出。 上面提到了DOT格式的图形。这个DOT[2]是什么呢?是graphviz[3]定义的一种图形描述语言,它可以通过graphviz提供的dot工具(安装graphviz之后就有了)把用DOT描述的图形转化为各种其他格式的图形。虽然有一些专门的DOT图形浏览工具,如dotty,不过这个东西不怎么好用,所以还是建议通过dot工具转换为比较常见的图片格式,如svg,jpg,gif,png,ps,它还可以转换成dia格式,进而可以通过“超级牛力”的dia绘图工具来进行进一步的编辑。
2. demo
下面来介绍这几个工具的具体用法,更多细节请参考它们自己的文档。 为了方便演示,这里写一个非常“糟糕的”但是却对这次演示很有用的代码。
/** * test.c -- * a demo program for using calltree, gprof&kprof, gdb&backtrace command * */
void a(void), b(void), c(void), d(void), e(void);
void a(void) { b (); } void b(void) { c (); } void c(void) { d (); e (); } void d(void) { ; } void e(void) { ; }
int main (int argc, char **argv) { if (argc < 2) { a (); } else { b (); } }
对这个代码而言,非常容易看出其调用关系,即: 当main的参数个数少于2个时,调用关系为 a -> b -> c -> d,e 当main的参数个数大于3个时,则调用关系为b -> c -> d,e 不过,如果一个项目包含几十个文件或者几千行甚至上万行代码,这个调用关系恐怕就没这么容易看出来了,所以还得借助后面的工具。
2.1 不用运行程序就可以打印整个项目的函数调用关系图: calltree
下载calltree后自己先编译安装好,放到/usr/bin下面。然后通过"calltree -help"查看该工具的帮助,这里通过使用-mb参数打印以main为树根的函数调用关系图。 Quote: $ calltree -mb test.c main: | a | | b | | | c | | | | d | | | | e | b | | c | | | d | | | e
从这个结果可以非常方便的看出函数调用关系,不过还是不够美观哦,所以加上-dot参数,产生一个dot图形吧。 Quote: $ calltree -mb test.c -dot > test.dot
okay,现在得到了一个关系调用图,即test.dot,因为这个格式不太常用,我们给它转换成jpg,见附图calltree.jpg。
Quote: $ dot -Tjpg test.dot -o calltree.jpg
不过貌似函数d和e没有打印出来,所以这个应该说是值得改进一下。还好我之前专门写了一个脚本,可以产生完整的输出,这个脚本见附件 tree2dot.sh.tar.gz,具体原理见资料[5],附图calltree1.jpg是这个脚本产生的。这里简单介绍它的用法:
Quote: 先通过脚本tree2dot.sh得到一个DOT图形 $ calltree -mb test.c | ./tree2dot.sh > test.dot 然后用dot转换为jpg格式 $ dot -Tjpg test.dot -o calltree1.jpg
2.2 打印项目当次运行过程中的函数调用关系图: gprof & kprof
首先通过gcc加上-pg参数编译程序(如果编译和链接分开,都需要加上该参数)。这个参数就是为了产生一些用于gprof&kprof的信息。gprof只有字符界面,而kprof提供了图形界面,下面仅介绍kprof,因为它和gprof相比,可以产生图形化的函数调用关系。
Quote: $ gcc -pg -o test test.c
编译完以后,运行一下就可以产生一个名为gmon.out的文件,它记录了该项目当次运行过程中的相关信息,包括函数调用关系。
Quote: $ ./test $ls gmon.out gmon.out
这样我们就可以用kprof来得到这个项目在这次运行过程中的函数调用关系图了( 实际上指定./test是为了告诉kprof,gmon.out和test在同一个目录下,kprof会去找gmon.out)。
Quote: $ kprof -f ./test
启动kprof以后找到Graph View标签,可以看到一个函数调用关系图。如果要把这个图导出来,找到Tools菜单,点击Generate Call Graph就可以导出一个DOT图形,我们命名为kprof_noargument.dot,然后我们就可以类似2.1用dot工具把它转换为其他格式,得到的效果图如kprof_noargument.jpg。
Quote: $ dot -Tjpg kprof_noargument.dot -o kprof_noargument.jpg
在上面,我们直接键入了./test执行它,如果给它传递上两个参数呢,这个时候argc等于二,在main中就不会再调用a函数,而是调用c函数,这样的话,函数调用关系图就不一样了,这次得到的结果图如kprof_twoargument.jpg。
Quote: 带上两个参数运行test $ ./test 1 2 通过kprof来查看调用关系图,并导出一个名为kprof_twoargument.dot的图形 $ kprof -f ./test 把DOT图形转换为jpg格式 $ dot -Tjpg kprof_twoargument.dot -o kprof_twoargument.jpg
这里没有提到gprof,因为它只产生一些不太好看的文本调用关系图,所以没有演示,不过它还是有很大作用的,具体参考一下资料[4]吧。 结合2.1和2.2,我们可以发现calltree和kprof两者都能够得到项目的函数调用关系图,不过前者能够得到整个项目的函数调用关系,而后者则能够得到某次运行过程中的函数调用关系,各有不同作用。通过前者我们可以了解整个项目的框架;而通过后者,我们可以找出一个项目在某些测试条件下的执行路径,从而更好地辅助源代码的分析。 有时候,这两种结果还是无法满足我们的要求,比如在调试过程中,我们设置了一个断点,并想了解一下这之前执行过哪些函数,进而找出潜在的bug可能出现的位置。
2.3 项目调试过程中打印某个位置(如断点)之前的函数调用关系图:gdb & backtrace command
为了能够用gdb调试程序,编译时请使用-g选项。
Quote: $ gcc -g -o test test.c
通过gdb的backtrace命令打印程序执行到某个位置之前的函数调用信息。
Quote: $ gdb ./test ... (gdb) set args 1 2 //这里设置为两个参数,所以选择了路径b->c->d,e (gdb) l 6 7 void a(void) { b (); } 8 void b(void) { c (); } 9 void c(void) { d (); e (); } 10 void d(void) { ; } 11 void e(void) { ; } 12 13 int main (int argc, char **argv) 14 { 15 if (argc < 2) { (gdb) break c Breakpoint 1 at 0x804833c: file test.c, line 9. (gdb) r Starting program: /home/falcon/Programming/test 1 2
Breakpoint 1, c () at test.c:9 9 void c(void) { d (); e (); } (gdb) backtrace #0 c () at test.c:9 #1 0x08048334 in b () at test.c:8 #2 0x08048380 in main (argc=3, argv=0xbf924e24) at test.c:18
在上面的调试过程中,我们首先通过set命令设置了两个参数,选择了main函数的第二个分支,并在c函数的入口设置了一个断点,然后运行程序直到该断点处,之后通过backtrace命令打印出之前的函数调用信息。通过最后几行,我们看到c最后被调用,之前是b,再之前是main。
到这里,开头提到的三种情况都介绍完了。不过呢,除了上面这些跟函数关系紧密的工具外,还有一个叫cscope[6]的工具,结合它和vim编辑器,在我们阅读源代码的过程中,可以利用它提供的":cs find d function"命令打印出函数function调用的所有函数,从而帮助我们了解某个函数内部的函数调用关系。当然该工具还有更丰富的用法,具体参考资料[6]。 除了这些分析应用程序的工具外,还有一个叫KFT[7]的工具可以用来分析linux内核。作为linux内核的一个补丁,它能够跟踪内核中某个系统调用的函数调用关系图,通过KFT提供的一个kd工具,可以得到一个文本格式的函数调用关系图,结合我上面用到的tree2dot.sh(建议用资料[5]中的tree2dot.sh),可以得到一个图形输出。
更多相关资料见后面。有任何建议和疑问,欢迎回帖交流,也可以直接给我发邮件。
PS: QQ不大好使,建议不要加我QQ号。
参考资料
[1] 使用Gnu gprof进行Linux平台下的程序分析 [2] The DOT Language http://www.graphviz.org/doc/info/lang.html [3] Graphviz - Graph Visualization Software http://www.graphviz.org/ [4] Coverage Measurement and Profiling http://www.linuxjournal.com/article/6758 [5] 用Graphviz进行可视化操作──绘制函数调用关系图 [6] cscope http://cscope.sourceforge.net/ [7] KFT(Kernel Function Tracing) http://elinux.org/Kernel_Function_Trace ftp://dslab.lzu.edu.cn/pub/kft
11月10日 #include <stdio.h> #include <malloc.h> #include <unistd.h> int bss_var; int data_var0 = 1; int main(int argc, char* argv[]) { //output process address information printf("below are addresses of types of process's mem\n"); printf("\t Address of main(Code Segment): %p\n", main); //stack address printf("--------------------------\n"); int stack_var0 = 2; printf("Stack Location : \n"); printf("\t Initial end of Stack: %p\n", &stack_var0); int stack_var1 = 3; printf("\t new end of stack : %p\n", &stack_var1); //data segment address printf("--------------------------\n"); printf("Data Location: \n"); printf("\t Address of data_var(Data Segment) :%p\n", &data_var0); static int data_var1 = 4; printf("\t New end of data_var(Data Segment) :%p\n", &data_var1); printf("----------------------------\n"); printf("BSS Location: \n"); printf("\t Address of bss_var: %p\n", &bss_var); //heap address printf("----------------------------\n"); char* b = sbrk((ptrdiff_t)0); printf("Heap Location: \n"); printf("\t Initial end of heap : %p\n", b); brk(b+4); b = sbrk((ptrdiff_t)0); printf("\t New end of heap: %p\n", b); return 0; } 11月9日
结构化程序的一个最基本的单元就是“函数”或者叫“过程”。在汇编这一层自然也相应的有支持这些概念的指令操作,如栈操作和栈帧的概念。
首先这里要为“打开汇编之门”那篇blog补充一点的是:汇编语言是与机器相关,这里的一切都是基于IA-32机器平台的。
1、寻址方式 我们已经知道在操作数表示中有一种是用来指示内存地址的内容的,在GNU Assembly中指示内存地址有多种方式,这些方式被统称“寻址方式”。通用的寻址格式为:“Imm(Eb, Ei, s)”[1]。解释一下:该表达式的计算方式为Imm + R[Eb] + R[Ei] * s,这一串的结果是什么呢?是一个存储器的地址,操作指令通过该操作数表达式计算出来的内存地址来访问内存。
由通用形式演化几种常见特殊形式如下: 1) Imm - 注意与$Imm区别,后者为立即数,而前者是以立即数形式承载的一个内存地址,这种方式叫绝对寻址; 2) (Ex) - 注意与Ex区别,后者为寄存器内容,而前者是以寄存器内容形式承载的一个内存地址,这种方式叫间接寻址; 3) Imm(Eb) - 其表示结果是内存地址为Imm + R[Eb]; 4) (Eb, Ei) - 其表示结果是内存地址为R[Eb] + R[Ei]; 5) Imm((Eb, Ei) - 其表示结果是内存地址为Imm + R[Eb] + R[Ei]。
2、寄存器使用 在“打开汇编之门”中曾经提过虽然寄存器的专用性已经降低,但是某些寄存器还是有其专用场合的。GNU为我们制定了一个寄存器使用规则,规则规定:“%eax、%ecx和%edx是由调用者负责存储的,而%ebx、%ebi和%esi则由被调用者保护,而%esp和%ebp都是栈操作专用的”。
3、栈操作 栈,实际上是一块儿专用的内存区域,每个进程地址空间都有其专有的栈区。地球人都知道关于栈有两种操作:Push和Pop。相应的GNU Assembly分别定义了“pushl S”和“popl D”分别来完成压栈和出栈操作。每个操作都包含两个步骤:移动栈顶指针和数据传送。 pushl S <=> R[%esp] <-- R[%esp] - 4 ;M[R[%esp]]<-- S popl D <=> D <-- M[R[%esp]];R[%esp] <-- R[%esp] + 4
4、栈帧的形成 提到函数或者过程调用就不能离开栈操作。而每个函数或者过程调用也都离不开一个叫“栈帧”的概念。栈是用来传递参数、保存返回结果等作用的,而栈帧则是1对1映射到某个过程调用的。栈帧由%ebp来标识。我们来看看一个例子,通过该例子看看栈帧里到底有些什么东西? void callee(int x, int y) { x = 1; y = 2; }
void caller(int m, int n) { callee(m, n); }
翻译为汇编代码为: _callee: pushl %ebp //保存调用者的栈帧地址 movl %esp, %ebp //初始化callee栈帧地址 movl $1, 8(%ebp) //获取参数x信息 movl $2, 12(%ebp) //获取参数y信息 popl %ebp ret ... ... ... ... _caller: pushl %ebp //保存调用者的栈帧地址 movl %esp, %ebp //初始化caller栈帧地址 subl $8, %esp movl 12(%ebp), %eax movl %eax, 4(%esp) movl 8(%ebp), %eax movl %eax, (%esp) call _callee leave ret 看看callee的汇编码:进入callee后首先保存其调用者caller的栈帧地址,然后读取其调用者caller栈帧中的参数信息进行计算。可以看出一个过程的栈帧中起码包括其上一个栈帧的起始地址,然后是一些参数信息,按照CS.APP说法,栈帧在存储参数信息之前还有可能保存一些本地变量或临时变量等。在每个过程的栈帧的结尾处都记录着过程返回地址,这个返回地址是由call执行时自动加入的。callee都是通过%ebp +/- 偏移量来获取参数信息的。用下面的图可以小结一下栈帧的模样(起始:%ebp所指的字节--> 终止:返回地址所在字节):
+ + | | +----------+ | old %ebp | <--- %ebp +----------+ | 本地变量 | +----------+ | 参数n | +----------+ | 参数...| +----------+ | 参数1 | +----------+ | 返回地址 | +----------+ | ... | | |<-- %esp
[注1] 这里采用了CS.APP中的表示方法,Eb表示基址寄存器,Ei表示变址寄存器,s为伸缩因子。我们使用R来表示引用某个寄存器的值,使用M来表示引用某内存地址。
原文:http://bigwhite.blogbus.com/logs/2005/11/1592114.html
_callee: pushl %ebp //保存调用者的栈帧地址 movl %esp, %ebp //初始化callee栈帧地址 movl $1, 8(%ebp) //获取参数x信息 movl $2, 12(%ebp) //获取参数y信息 popl %ebp ret ==================== 掉了一句movl %ebp,%esp,放在popl %ebp之前,不然栈无法平衡了 11月4日 arm平台下使用bl和ldr跳转应当注意的地方(arm-linux-gcc环境)
一,按lds文件连接的不同模块,不能用bl实现跳转
一个错误的例子: 1.crt0.s @****************************************************************************** @ File:crt0.s @ 功能:通过它转入C程序 @****************************************************************************** .extern main .text .global _start _start: ldr sp, =1024*4 @设置堆栈,注意:不能大于4k , @这儿堆栈可以设置为0x34000000,根据内存地址空间分配确定 bl main @调用C程序中的main函数 halt_loop: b halt_loop
2.leds.c
@****************************************************************************** @ file leds.c @ main函数 @****************************************************************************** int main() { __asm__ (
" ldrb r0, [r1], #1\n" " strb r0, [r2], #1\n"
);
return 0; }
3.Makefile
CFLAGS := -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -ffreestanding -c leds : crt0.s leds.c arm-linux-gcc $(CFLAGS) -o crt0.o crt0.s arm-linux-gcc $(CFLAGS) -o leds.o leds.c arm-linux-ld -Tleds.lds crt0.o leds.o -o leds_tmp.o arm-linux-objcopy -O binary -S leds_tmp.o leds arm-linux-objdump -D -b binary -m arm leds >ttt.s clean: rm -f leds rm -f leds.o rm -f leds_tmp.o rm -f crt0.o
4.leds.lds 文件
SECTIONS { firtst 0x00000000 : { crt0.o } second 0x00000000 : AT(0x0100) { leds.o } }
5.反汇编代码ttt.s 00000000 <.data>: 0: e3a0da01 mov sp, #4096 ; 0x1000 4: ebfffffd bl 0x0 //这儿不能调转到main 因为bl跳转有限制 8: eafffffe b 0x8 ... 100: e24dd040 sub sp, sp, #64 ; 0x40 104: e3a00000 mov r0, #0 ; 0x0 108: e28dd040 add sp, sp, #64 ; 0x40 10c: e1a0f00e mov pc, lr 110: 43434700 cmpmi r3, #0 ; 0x0 114: 4728203a undefined 118: 2029554e eorcs r5, r9, lr, asr #10 11c: 2e332e33 mrccs 14, 1, r2, cr3, cr3, {1} 120: 00000032 andeq r0, r0, r2, lsr r0
通过上面的例子可以看到crt0中的bl main出错 "4: ebfffffd bl 0x0 " bl没有成功。
6.改正方法1:
原lds文件把俩个目标文件分开排列,这里把俩个目标文件指定到一起,这样不能重定位。 修改后的lds文件 SECTIONS { firtst 0x00000000 : { crt0.o leds.o } }
改正后的效果 0: e3a0da01 mov sp, #4096 ; 0x1000 4: eb000000 bl 0xc //这里bl跳转到正确的地址 8: eafffffe b 0x8 c: e24dd040 sub sp, sp, #64 ; 0x40 10: e3a00000 mov r0, #0 ; 0x0 14: e28dd040 add sp, sp, #64 ; 0x40 18: e1a0f00e mov pc, lr 1c: 43434700 cmpmi r3, #0 ; 0x0 20: 4728203a undefined 24: 2029554e eorcs r5, r9, lr, asr #10 28: 2e332e33 mrccs 14, 1, r2, cr3, cr3, {1} 2c: 00000032 andeq r0, r0, r2, lsr r0
二,使用ldr命令来实现长跳转(改正方法2)
1. ldr pc, =main @调用C程序中的main函数 通过ldr 对pc赋值来实现跳转
@****************************************************************************** @ File:crt0.s @ 功能:通过它转入C程序 @****************************************************************************** .extern main .text .global _start _start: ldr sp, =1024*4 @设置堆栈,注意:不能大于4k,nand flash中的代码在复位后会移到内部ram中,此ram只有4k ldr pc, =main @调用C程序中的main函数 halt_loop: b halt_loop 2.leds.lds文件 SECTIONS { firtst 0x00000000 : { crt0.o } second 0x30000000 : AT(0x1000) { leds.o } }
3.反汇编结果 00000000 <.data>: 0: e3a0da01 mov sp, #4096 ; 0x1000 4: e59ff000 ldr pc, [pc, #0] ; 0xc 8: eafffffe b 0x8 c: 30000000 andcc r0, r0, r0 ... 1000: e4d10001 ldrb r0, [r1], #1 1004: e4c20001 strb r0, [r2], #1 1008: e3a00000 mov r0, #0 ; 0x0 100c: e1a0f00e mov pc, lr 1010: 43434700 cmpmi r3, #0 ; 0x0 1014: 4728203a undefined 1018: 2029554e eorcs r5, r9, lr, asr #10 101c: 2e332e33 mrccs 14, 1, r2, cr3, cr3, {1} 1020: 00000032 andeq r0, r0, r2, lsr r0
可以看到4: e59ff000 ldr pc, [pc, #0] ; 0xc 这条命令将跳转到0x30000000处开始执行 第一部分 Linux下ARM汇编语法 尽管在Linux下使用C或C++编写程序很方便,但汇编源程序用于系统最基本的初始化,如初始化堆栈指针、设置页表、操作ARM的协处理器等。初始化完成后就可以跳转到C代码执行。需要注意的是,GNU的汇编器遵循AT&T的汇编语法,可以从GNU的站点(www.gnu.org)上下载有关规范。
一. Linux汇编行结构 任何汇编行都是如下结构: [:] [} @ comment [:] [} @ 注释 Linux ARM 汇编中,任何以冒号结尾的标识符都被认为是一个标号,而不一定非要在一行的开始。 【例1】定义一个"add"的函数,返回两个参数的和。 .section .text, “x” .global add @ give the symbol add external linkage add: ADD r0, r0, r1 @ add input arguments MOV pc, lr @ return from subroutine @ end of program
二. Linux 汇编程序中的标号 标号只能由a~z,A~Z,0~9,“.”,_等字符组成。当标号为0~9的数字时为局部标号,局部标号可以重复出现,使用方法如下: 标号f: 在引用的地方向前的标号 标号b: 在引用的地方向后的标号 【例2】使用局部符号的例子,一段循环程序 1: subs r0,r0,#1 @每次循环使r0=r0-1 bne 1f @跳转到1标号去执行 局部标号代表它所在的地址,因此也可以当作变量或者函数来使用。
三. Linux汇编程序中的分段 (1).section伪操作 用户可以通过.section伪操作来自定义一个段,格式如下: .section section_name [, "flags"[, %type[,flag_specific_arguments]]] 每一个段以段名为开始, 以下一个段名或者文件结尾为结束。这些段都有缺省的标志(flags),连接器可以识别这些标志。(与armasm中的AREA相同)。
下面是ELF格式允许的段标志 <标志> 含义 a 允许段 w 可写段 x 执行段
【例3】定义段 .section .mysection @自定义数据段,段名为 “.mysection” .align 2 strtemp: .ascii "Temp string \n\0"
(2)汇编系统预定义的段名 .text @代码段 .data @初始化数据段 .bss @未初始化数据段 .sdata @ .sbss @ 需要注意的是,源程序中.bss段应该在.text之前。 四. 定义入口点 汇编程序的缺省入口是 start标号,用户也可以在连接脚本文件中用ENTRY标志指明其它入口点。 【例4】定义入口点 .section.data < initialized data here> .section .bss < uninitialized data here> .section .text .globl _start _start: <instruction code goes here>
五. Linux汇编程序中的宏定义 格式如下: .macro 宏名 参数名列表 @伪指令.macro定义一个宏 宏体 .endm @.endm表示宏结束 如果宏使用参数,那么在宏体中使用该参数时添加前缀“\”。宏定义时的参数还可以使用默认值。 可以使用.exitm伪指令来退出宏。 【例5】宏定义 .macro SHIFTLEFT a, b .if \b < 0 MOV \a, \a, ASR #-\b .exitm .endif MOV \a, \a, LSL #\b .endm
六. Linux汇编程序中的常数 (1)十进制数以非0数字开头,如:123和9876; (2)二进制数以0b开头,其中字母也可以为大写; (3)八进制数以0开始,如:0456,0123; (4)十六进制数以0x开头,如:0xabcd,0X123f; (5)字符串常量需要用引号括起来,中间也可以使用转义字符,如: “You are welcome!\n”; (6)当前地址以“.”表示,在汇编程序中可以使用这个符号代表当前指令的地址; (7)
表达式:在汇编程序中的表达式可以使用常数或者数值, “-”表示取负数,
“~”表示取补,“<>”表示不相等,其他的符号如:+、-、*、
/、%、<、<<、>、>>、|、&、^、!、==、>=、<=、&&、||
跟C语言中的用法相似。
七. Linux下ARM汇编的常用伪操作 在前面已经提到过了一些为操作,还有下面一些为操作: 数据定义伪操作: .byte,.short,.long,.quad,.float,.string/.asciz/.ascii,重复定义伪操作.rept,赋值语句.equ/.set ; 函数的定义 ; 对齐方式伪操作 .align; 源文件结束伪操作.end; .include伪操作; if伪操作; .global/ .globl 伪操作 ; .type伪操作 ; 列表控制语句 ; 区别于gas汇编的通用伪操作,下面是ARM特有的伪操作 :.reg ,.unreq ,.code ,.thumb ,.thumb_func ,.thumb_set, .ltorg ,.pool 1. 数据定义伪操作 (1) .byte:单字节定义,如:.byte 1,2,0b01,0x34,072,'s' ; (2) .short:定义双字节数据,如:.short 0x1234,60000 ; (3) .long:定义4字节数据,如:.long 0x12345678,23876565 (4) .quad:定义8字节,如:.quad 0x1234567890abcd (5) .float:定义浮点数,如: .float 0f-314159265358979323846264338327\ 95028841971.693993751E-40 @ - pi (6) .string/.asciz/.ascii:定义多个字符串,如: .string "abcd", "efgh", "hello!" .asciz "qwer", "sun", "world!" .ascii "welcome\0" 需要注意的是:.ascii伪操作定义的字符串需要自行添加结尾字符'\0'。 (7) .rept:重复定义伪操作, 格式如下: .rept 重复次数 数据定义 .endr @结束重复定义 例如: .rept 3 .byte 0x23 .endr (8) .equ/.set: 赋值语句, 格式如下: .equ(.set) 变量名,表达式 例如: .equ abc 3 @让abc=3
2.函数的定义伪操作 (1)函数的定义,格式如下: 函数名: 函数体 返回语句 一般的,函数如果需要在其他文件中调用, 需要用到.global伪操作将函数声明为全局函数。为了不至于在其他程序在调用某个C函数时发生混乱,对寄存器的使用我们需要遵循APCS准则。函数编译器将处理为函数代码为一段.global的汇编码。 (2)函数的编写应当遵循如下规则: a1-a4寄存器(参数、结果或暂存寄存器,r0到r3 的同义字)以及浮点寄存器f0-f3(如果存在浮点协处理器)在函数中是不必保存的; 如果函数返回一个不大于一个字大小的值,则在函数结束时应该把这个值送到 r0 中; 如果函数返回一个浮点数,则在函数结束时把它放入浮点寄存器f0中; 如果函数的过程改动了sp(堆栈指针,r13)、fp(框架指针,r11)、sl(堆栈限制,r10)、lr(连接寄存器,r14)、v1-v8(变量寄存器,r4 到 r11)和 f4-f7,那么函数结束时这些寄存器应当被恢复为包含在进入函数时它所持有的值。
3. .align .end .include .incbin伪操作 (1).align:用来指定数据的对齐方式,格式如下: .align [absexpr1, absexpr2] 以某种对齐方式,在未使用的存储区域填充值. 第一个值表示对齐方式,4, 8,16或 32. 第二个表达式值表示填充的值。 (2).end:表明源文件的结束。 (3).include:可以将指定的文件在使用.include 的地方展开,一般是头文件,例如: .include “myarmasm.h” (4).incbin伪操作可以将原封不动的一个二进制文件编译到当前文件中,使用方法如下: .incbin "file"[,skip[,count]] skip表明是从文件开始跳过skip个字节开始读取文件,count是读取的字数.
4. .if伪操作 根据一个表达式的值来决定是否要编译下面的代码, 用.endif伪操作来表示条件判断的结束, 中间可以使用.else来决定.if的条件不满足的情况下应该编译哪一部分代码。 .if有多个变种: .ifdef symbol @判断symbol是否定义 .ifc string1,string2 @字符串string1和string2是否相等,字符串可以用单引号括起来 .ifeq expression @判断expression的值是否为0 .ifeqs string1,string2 @判断string1和string2是否相等,字符 串必须用双引号括起来 .ifge expression @判断expression的值是否大于等于0 .ifgt absolute expression @判断expression的值是否大于0 .ifle expression @判断expression的值是否小于等于0 .iflt absolute expression @判断expression的值是否小于0 .ifnc string1,string2 @判断string1和string2是否不相等, 其用法跟.ifc恰好相反。 .ifndef symbol, .ifnotdef symbol @判断是否没有定义symbol, 跟.ifdef恰好相反 .ifne expression @如果expression的值不是0, 那么编译器将编译下面的代码 .ifnes string1,string2 @如果字符串string1和string2不相 等, 那么编译器将编译下面的代码.
5. .global .type .title .list (1).global/ .globl :用来定义一个全局的符号,格式如下: .global symbol 或者 .globl symbol (2).type:用来指定一个符号的类型是函数类型或者是对象类型, 对象类型一般是数据, 格式如下: .type 符号, 类型描述 【例6】 .globl a .data .align 4 .type a, @object .size a, 4 a: .long 10 【例7】 .section .text .type asmfunc, @function .globl asmfunc asmfunc:
mov pc, lr
(3)列表控制语句: .title:用来指定汇编列表的标题,例如: .title “my program” .list:用来输出列表文件.
6. ARM特有的伪操作 (1) .reg: 用来给寄存器赋予别名,格式如下: 别名 .req 寄存器名 (2) .unreq: 用来取消一个寄存器的别名,格式如下: .unreq 寄存器别名 注意被取消的别名必须事先定义过,否则编译器就会报错,这个伪操作也可以用来取消系统预制的别名, 例如r0, 但如果没有必要的话不推荐那样做。 (3) .code伪操作用来选择ARM或者Thumb指令集,格式如下: .code 表达式 如果表达式的值为16则表明下面的指令为Thumb指令,如果表达式的值为32则表明下面的指令为ARM指令. (4) .thumb伪操作等同于.code 16, 表明使用Thumb指令, 类似的.arm等同于.code 32 (5) .force_thumb伪操作用来强制目标处理器选择thumb的指令集而不管处理器是否支持 (6) .thumb_func伪操作用来指明一个函数是thumb指令集的函数 (7) .thumb_set伪操作的作用类似于.set, 可以用来给一个标志起一个别名, 比.set功能增加的一点是可以把一个标志标记为thumb函数的入口, 这点功能等同于.thumb_func (8) .ltorg用于声明一个数据缓冲池(literal pool)的开始,它可以分配很大的空间。 (9) .pool的作用等同.ltorg (9).space <number_of_bytes> {,<fill_byte>} 分配number_of_bytes字节的数据空间,并填充其值为fill_byte,若未指定该值,缺省填充0。(与armasm中的SPACE功能相同) (10).word <word1> {,<word2>} … 插入一个32-bit的数据队列。(与armasm中的DCD功能相同) 可以使用.word把标识符作为常量使用 例如: Start: valueOfStart: .word Start 这样程序的开头Start便被存入了内存变量valueOfStart中。 (11).hword <short1> {,<short2>} … 插入一个16-bit的数据队列。(与armasm中的DCW相同)
八. GNU ARM汇编特殊字符和语法 代码行中的注释符号: ‘@’ 整行注释符号: ‘#’ 语句分离符号: ‘;’ 直接操作数前缀: ‘#’ 或 ‘$’
第二部分 GNU的编译器和调试工具
一. 编译工具 1.编辑工具介绍 GNU提供的编译工具包括汇编器as、C编译器gcc、C++编译器g++、连接器ld和二进制转
换工具objcopy。基于ARM平台的工具分别为arm-linux-as、arm-linux-gcc、arm-linux-g++、arm-
linux-ld和arm-linux-
objcopy。GNU的编译器功能非常强大,共有上百个操作选项,这也是这类工具让初学者头痛的原因。不过,实际开发中只需要用到有限的几个,大部分可
以采用缺省选项。GNU工具的开发流程如下:编写C、C++语言或汇编源程序,用gcc或g++生成目标文件,编写连接脚本文件,用连接器生成最终目标文
件(elf格式),用二进制转换工具生成可下载的二进制代码。 (1)编写C、C++语言或汇编源程序 通常汇编源程序用于系统最基本的初始化,如初始化堆栈指针、设置页表、操作ARM的协处理器等。初始化完成后就可以跳转到C代码执行。需要注意的是,GNU的汇编器遵循AT&T的汇编语法,读者可以从GNU的站点(www.gnu.org)上下载有关规范。汇编程序的缺省入口是 start标号,用户也可以在连接脚本文件中用ENTRY标志指明其它入口点(见下文关于连接脚本的说明)。
(2)用gcc或g++生成目标文件 如果应用程序包括多个文件,就需要进行分别编译,最后用连接器连接起来。如笔者的引导程序包括3个文件:init.s(汇编代码、初始化硬件)xmrecever.c(通信模块,采用Xmode协议)和flash.c(Flash擦写模块)。 分
别用如下命令生成目标文件: arm-linux-gcc-c-O2-oinit.oinit.s
arm-linux-gcc-c-O2-oxmrecever.oxmrecever.c
arm-linux-gcc-c-O2-oflash.oflash.c
其中-c命令表示只生成目标代码,不进行连接;-o命令指明目标文件的名称;-O2表示采用二级优化,采用优化后可使生成的代码更短,运行速度更快。如果
项目包含很多文件,则需要编写makefile文件。关于makefile的内容,请感兴趣的读者参考相关资料。 (3)编写连接脚本文件 gcc
等编译器内置有缺省的连接脚本。如果采用缺省脚本,则生成的目标代码需要操作系统才能加载运行。为了能在嵌入式系统上直接运行,需要编写自己的连接脚本文
件。编写连接脚本,首先要对目标文件的格式有一定了解。GNU编译器生成的目标文件缺省为elf格式。elf文件由若干段(section)组成,如不特
殊指明,由C源程序生成的目标代码中包含如下段:.text(正文段)包含程序的指令代码;.data(数据段)包含固定的数据,如常量、字符
串;.bss(未初始化数据段)包含未初始化的变量、数组等。C++源程序生成的目标代码中还包括.fini(析构函数代码)和.
init(构造函数代码)等。连接器的任务就是将多个目标文件的.text、.data和.bss等段连接在一起,而连接脚本文件是告诉连接器从什么地址
开始放置这些段。例如连接文件link.lds为: ENTRY(begin) SECTION { .=0x30000000; .text:{*(.text)} .data:{*(.data)} .bss:{*(.bss)} } 其
中,ENTRY(begin)指明程序的入口点为begin标号;.=0x00300000指明目标代码的起始地址为0x30000000,这一段地址为
MX1的片内RAM;.text:{*(.text)}表示从0x30000000开始放置所有目标文件的代码段,随后的.data:{*
(.data)}表示数据段从代码段的末尾开始,再后是.bss段。 (4)用连接器生成最终目标文件 有了连接脚本文件,如下命令可生成最终的目标文件: arm-linux-ld –no stadlib –o bootstrap.elf -Tlink.lds init.o xmrecever.o flash.o 其中,ostadlib表示不连接系统的运行库,而是直接从begin入口;-o指明目标文件的名称;-T指明采用的连接脚本文件(也可以使用-Ttext address,address表示执行区地址);最后是需要连接的目标文件列表。 (5)生成二进制代码 连接生成的elf文件还不能直接下载执行,通过objcopy工具可生成最终的二进制文件: arm-linux-objcopy –O binary bootstrap.elf bootstrap.bin 其中-O binary指定生成为二进制格式文件。Objcopy还可以生成S格式的文件,只需将参数换成-O srec。还可以使用-S选项,移除所有的符号信息及重定位信息。如果想将生成的目标代码反汇编,还可以用objdump工具: arm-linux-objdump -D bootstrap.elf 至此,所生成的目标文件就可以直接写入Flash中运行了。
2.Makefile实例 example: head.s main.c arm-linux-gcc -c -o head.o head.s arm-linux-gcc -c -o main.o main.c arm-linux-ld -Tlink.lds head.o ain.o -o example.elf arm-linux-objcopy -O binary -S example_tmp.o example arm-linux-objdump -D -b binary -m arm example >ttt.s
二. 调试工具 Linux下的GNU调试工具主要是gdb、gdbserver和kgdb。其中gdb和gdbserver可完成对目标板
上Linux下应用程序的远程调试。gdbserver是一个很小的应用程序,运行于目标板上,可监控被调试进程的运行,并通过串口与上位机上的gdb通
信。开发者可以通过上位机的gdb输入命令,控制目标板上进程的运行,查看内存和寄存器的内容。gdb5.1.1以后的版本加入了对ARM处理器的支持,
在初始化时加入- target==arm参数可直接生成基于ARM平台的gdbserver。gdb工具可以从ftp:
//ftp.gnu.org/pub/gnu/gdb/上下载。 对于Linux内核的调试,可以采用kgdb工具,同样需要通过串口与上位机上的gdb通信,对目标板的Linux内核进行调试。可以从http://oss.sgi.com/projects/kgdb/上了解具体的使用方法。
参考资料: 1. Richard Blum,Professional Assembly Language 2. GNU ARM 汇编快速入门,http://blog.chinaunix.net/u/31996/showart.php?id=326146 3. ARM GNU 汇编伪指令简介,http://www.cppblog.com/jb8164/archive/2008/01/22/41661.aspx 4. GNU汇编使用经验,http://blog.chinaunix.net/u1/37614/showart_390095.html 5. GNU的编译器和开发工具,http://blog.ccidnet.com/blog-htm-do-showone-uid-34335-itemid-81387-type-blog.html 6. 用GNU工具开发基于ARM的嵌入式系统,http://blog.163.com/liren0@126/blog/static/32897598200821211144696/ 7. objcopy命令介绍,http://blog.csdn.net/junhua198310/archive/2007/06/27/1669545.aspx GCC内嵌汇编语言
作者:肖文鹏 临江仙 整理:杨小华
绝大多数 Linux 程序员以前只接触过DOS/Windows 下的汇编语言,这些汇编代码都是 Intel 风格的。但在 Unix 和 Linux 系统中,更多采用的还是 AT&T 格式,两者在语法格式上有着很大的不同。
汇编基本语法简介
在 AT&T 汇编格式中,寄存器名要加上 '%' 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。例如:
|
AT&T 格式
|
Intel 格式
|
|
pushl %eax
|
push eax
|
在 AT&T 汇编格式中,用 '$' 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。例如:
|
AT&T 格式
|
Intel 格式
|
|
pushl $1
|
push 1
|
AT&T 和 Intel 格式中的源操作数和目标操作数的位置正好相反。在 Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&T 汇编格式中,目标操作数在源操作数的右边。例如:
|
AT&T 格式
|
Intel 格式
|
|
addl $1, %eax
|
add eax, 1
|
在 AT&T
汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为字节(byte,8 比特)、字(word,16
比特)和长字(long,32比特);而在 Intel 汇编格式中,操作数的字长是用 "byte ptr" 和 "word ptr"
等前缀来表示的。例如:
|
AT&T 格式
|
Intel 格式
|
|
movb val, %al
|
mov al, byte ptr val
|
在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上'*'作为前缀,而在 Intel 格式中则不需要。
远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为 "ljump" 和 "lcall",而在 Intel 汇编格式中则为 "jmp far" 和 "call far",即:
|
AT&T 格式
|
Intel 格式
|
|
ljump $section, $offset
|
jmp far section:offset
|
|
lcall $section, $offset
|
call far section:offset
|
与之相应的远程返回指令则为:
|
AT&T 格式
|
Intel 格式
|
|
lret $stack_adjust
|
ret far stack_adjust
|
在 AT&T 汇编格式中,内存操作数的寻址方式是
|
AT&T 格式
|
Intel 格式
|
|
section:disp(base, index, scale)
|
section:[base + index*scale + disp]
|
由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:disp + base + index * scale
下面是一些内存操作数的例子:
|
AT&T 格式
|
Intel 格式
|
|
movl -4(%ebp), %eax
|
mov eax, [ebp - 4]
|
|
movl array(, %eax, 4), %eax
|
mov eax, [eax*4 + array]
|
|
movw array(%ebx, %eax, 4), %cx
|
mov cx, [ebx + 4*eax + array]
|
|
movb $4, %fs:(%eax)
|
mov fs:eax, 4
|
内嵌汇编格式简介
内嵌汇编语法如下:
|
__asm__(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)
|
其中,asm 和 __asm__是完全一样的。共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空。例如:
|
__asm__ __volatile__("cli": : :"memory")
|
1、汇编语句模板
汇编语句模板由汇编语句序列组成,语句之间使用
“;”、“\\n”或“\\n\\t”分开。指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1,…,%9。指
令中使用占位符表示的操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字
或者低字节。对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1。
2、输出部分
输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C 语言变量组成。每个输出操作数的限定字符串必须包含“=”表示他是一个输出操作数。
例:
|
__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )
|
描述符字符串表示对该变量的限制条件,这样GCC 就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。
3、输入部分
输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成。
例1 :
|
__asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt));
|
例二(bitops.h):
|
Static __inline__ void __set_bit(int nr, volatile void * addr)
{
__asm__(
"btsl %1,%0"
:"=m" (ADDR)
:"Ir" (nr));
}
|
后例功能是将(*addr)的第nr位设为
1。第一个占位符%0与C 语言变量ADDR对应,第二个占位符%1与C语言变量nr对应。因此上面的汇编语句代码与下面的伪代码等价:btsl
nr, ADDR,该指令的两个操作数不能全是内存变量,因此将nr的限定字符串指定为“Ir”,将nr
与立即数或者寄存器相关联,这样两个操作数中只有ADDR为内存变量。
4、限制字符
4.1、限制字符列表
限制字符有很多种,有些是与特定体系结构相关,此处仅列出常用的限定字符和i386中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的C语言变量与指令操作数之间的关系。
|
分类
|
限定符
|
描述
|
|
通用寄存器
|
a
|
将
输入变量放入eax这里有一个问题:假设eax已经被使用,那怎么办?其实很简单:因为GCC 知道eax
已经被使用,它在这段汇编代码的起始处插入一条语句pushl %eax,将eax 内容保存到堆栈,然后在这段代码结束处再增加一条语句popl
%eax,恢复eax的内容
|
|
b
|
将输入变量放入ebx
|
|
c
|
将输入变量放入ecx
|
|
d
|
将输入变量放入edx
|
|
s
|
将输入变量放入esi
|
|
d
|
将输入变量放入edi
|
|
q
|
将输入变量放入eax,ebx,ecx,edx中的一个
|
|
r
|
将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个
|
|
A
|
把eax和edx合成一个64 位的寄存器(use long longs)
|
|
内存
|
m
|
内存变量
|
|
o
|
操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
|
|
V
|
操作数为内存变量,但寻址方式不是偏移量类型
|
|
“”
|
操作数为内存变量,但寻址方式为自动增量
|
|
p
|
操作数是一个合法的内存地址(指针)
|
|
寄存器或内存
|
g
|
将输入变量放入eax,ebx,ecx,edx中的一个或者作为内存变量
|
|
X
|
操作数可以是任何类型
|
|
立即数
|
I
|
0-31之间的立即数(用于32位移位指令)
|
|
J
|
0-63之间的立即数(用于64位移位指令)
|
|
N
|
0-255之间的立即数(用于out指令)
|
|
i
|
立即数
|
|
n
|
立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”
|
|
匹配
|
0
|
表示用它限制的操作数与某个指定的操作数匹配,
|
|
1
|
也即该操作数就是指定的那个操作数,例如“0”
|
|
9
|
去描述“%1”操作数,那么“%1”引用的其实就是“%0”操作数,注意作为限定符字母的0-9 与指令中的“%0”-“%9”的区别,前者描述操作数,后者代表操作数。
|
|
&
|
该输出操作数不能使用过和输入操作数相同的寄存器
|
|
操作数类型
|
=
|
操作数在指令中是只写的(输出操作数)
|
|
+
|
操作数在指令中是读写类型的(输入输出操作数)
|
|
浮点数
|
f
|
浮点寄存器
|
|
t
|
第一个浮点寄存器
|
|
u
|
第二个浮点寄存器
|
|
G
|
标准的80387浮点常数
|
|
%
|
该操作数可以和下一个操作数交换位置 例如addl的两个操作数可以交换顺序(当然两个操作数都不能是立即数)
|
|
#
|
部分注释,从该字符到其后的逗号之间所有字母被忽略
|
|
*
|
表示如果选用寄存器,则其后的字母被忽略
|
5、破坏描述部分
破坏描述符用于通知编译器我们使用了哪些寄存器或内存,由逗号格开的字符串组成,每个字符串描述一种情况,一般是寄存器名;除寄存器外还有“memory”。例如:“%eax”,“%ebx”,“memory”等。
感谢
这篇文章是两篇文章的综合体,我只是把这两篇文章综合起来了,进行了一下简单的排版,阅读起来方便一点,舒服一点。两位作者分别是肖文鹏和临江仙,向他们表示感谢。后续我会根据相关资料,继续改进该文档,使之更全面。Mail:normalnotebook@126.com,互相学习。 1 ARM GNU 汇编伪指令简介 (1)abort .abort 停止汇编 (2)align .align absexpr1,absexpr2 以某种对齐方式,在未使用的存储区域填充值. 第一个值表示对齐方式,4, 8,16或 32. 第二个表达式值表示填充的值 (3)if...else...endif .if .else .endif: 支持条件预编译 (4)include .include "file": 包含指定的头文件, 可以把一个汇编常量定义放在头文件中 (5)comm .comm symbol, length: 在bss段申请一段命名空间,该段空间的名称叫symbol, 长度为length. Ld连接器在连接 会为它留出空间 (6)data .data subsection: 说明接下来的定义归属于subsection数据段 (7)equ .equ symbol, expression: 把某一个符号(symbol)定义成某一个值(expression).该 指令并不分配空间 (8)global .global symbol: 定义一个全局符号, 通常是为ld使用 (9)ascii .ascii "string": 定义一个字符串并为之分配空间 (10)byte .byte expressions: 定义一个字节, 并为之分配空间 (11)short .short expressions: 定义一个短整型, 并为之分配空间 (12)int .int expressions: 定义一个整型,并为之分配空间 (13)long .long expressions: 定义一个长整型, 并为之分配空间 (14)word .word expressions: 定义一个字,并为之分配空间, 4 bytes (15)macro/endm .macro: 定义一段宏代码, .macro表示代码的开始, .endm表示代码的结束, .exitm 跳出宏, 示例如下: .macro SHIFTLEFT a, b .if \b < 0 mov \a, \a, ASR #-\b .exitm .endif mov \a, \a, LSL #\b .endm (16)req name .req register name: 为寄存器定义一个别名 (17)code .code [16|32]: 指定指令代码产生的长度, 16表示Thumb指令, 32表示ARM指令 (18)ltorg .ltorg: 表示当前往下的定义在归于当前段,并为之分配空间
2 ARM GNU专有符号 (1)@ 表示注释从当前位置到行尾的字符. (2)# 注释掉一整行. (3); 新行分隔符.
3 操作码 (1)NOP: nop 空操作, 相当于MOV r0, r0 (2)LDR: ldr <register> , =<expression> 相当于PC寄存器或其它寄存器的长转移 (3)ADR: adr <register> <label> 相于PC寄存器或其它寄存器的小范围转移 (4)ADRL: adrl <register> <label> 相于PC寄存器或其寄存器的中范围转移
|