1.本文不是教程,只是描述c語言(gcc環境),編譯器,連接器,加載器,at&t匯編,ia32一些相關知識和筆記,很多需要深入的地方需要大家尋找相關的資料學習。如果發現錯誤,請留言或通知我jinglexy at yahoo dot com dot cn,這個是我的msn。打字不易,請轉載時保留作者:http://www.shnenglu.com/jinglexy
2.gcc安裝的各個部分:
|
g++
|
c++編譯器,鏈接時使用c++庫
|
|
gcc
|
c編譯器,鏈接時使用c庫
|
|
cc1
|
實際的c編譯器
|
|
cc1plus
|
實際的c++編譯器
|
|
collect2
|
使用collect2產生特定的全局初始化代碼,后臺處理是傳遞參數給ld完成實際的鏈接工作。
|
|
crt0.o
|
初始化和結束代碼
|
|
libgcc
|
平臺相關的庫
|
gcc安裝需要的文件:
gcc-core-3.4.6.tar.gz2
gcc核心編譯器,默認只包含c編譯器
gcc-g++-3.4.6.tar.bz2
g++編譯器
gcc-testsuite-3.4.6.tar.bz2
測試套件
./configure && make && make install
3.binutils安裝的各個部分
|
as
|
gnu匯編工具
|
|
gprof
|
性能分析工具
|
|
ld
|
gnu鏈接器
|
|
make
|
|
|
objcopy
|
目標文件從二進制格式翻譯或復制到另一種
|
|
objdump
|
顯示目標文件的各種信息
|
|
strings
|
顯示文件的字符串
|
|
strip
|
去除符合表
|
|
readelf
|
分析elf并顯示信息
|
鏈接器可以讀寫各種目標文件中的信息,通過BFD(binary file descriptor)提供的工具實現,BFD定義了類似a.out, elf, coff等目標文件的格式。
4.gcc預處理程序
1)define指令
#可將傳遞的宏字符串化
##將兩個名字連接成一個(注意不是連接成字符串)
例:#define
TEST(ARGTERM) \
printf(“the
term “ #ARGTERM “is a string\n”)
使用__VA_ARGS__定義可變參數宏
例:#define err(...) fprintf(stderr,
__VA_ARGS)
err (“%s %d\n”, “error code is”,
48);
為了消除無參數時的逗號,可以用下面方法定義:
# define
err(...) fprintf(stderr,
##__VA_ARGS)
一種等同的方法是:
#define dprintf(fmt,
arg...) printf(fmt, ##arg)
其他例:#define PASTE(a,
b) a##b
2)error 和 warning指令
#error “y here? bad boy!”
3)if, elif, else, endif指令
支持的運算符:加減乘除,位移,&&,||,!等
示例:#if defined (CONFIG_A) || defined (CONFIG_B)
……
#endif
4)gcc預定義宏
|
__BASE_FILE__
|
完整的源文件名路徑
|
|
__cplusplus
|
測試c++程序
|
|
__DATE__
|
|
|
__FILE__
|
源文件名
|
|
__func__
|
替代__FUNCTION__,__FUNCTION__以被GNU不推薦使用
|
|
__TIME__
|
|
|
__LINE__
|
|
|
__VERSION__
|
gcc版本
|
|
|
|
5)幾個簡單例子:
例1:
#define min(X, Y) \
(__extension__ ({typeof (X) __x = (X), __y = (Y);
\
(__x < __y) ? __x : __y; }))
#define max(X, Y) \
(__extension__ ({typeof (X) __x = (X), __y = (Y);
\
(__x > __y) ? __x : __y; }))
這樣做的目的是消除宏對X,Y的改變的影響,例如:result = min(x++, --y); printf(x, y);
補充:圓括號定義的符合語句可以生成返回值,例:
result = ({ int a = 5;
int b;
b = a + 3;
}); 將返回8
例2:
#define dprintfbin(buf, size) do{ int i;
\
printf("%s(%d)@", \
__FUNCTION__,
__LINE__);
\
for(i = 0; i < size -
1; i++){ \
if(0
== i %
16) \
printf("\n"); \
printf("0x%02x
", ((char*)buf)[i]); \
} \
printf("0x%02x\n",
((char*)buf)[i]); \
}while(0)
這個比較簡單,不用解釋了
例3:
#ifdef __cplusplus
extern "C"{
#endif
int foo1(void);
int foo2(void);
#ifdef __cplusplus
}
#endif
作用:在c++程序中使用c函數及庫,c++編譯程序時將函數名粉碎成自己的方式,在沒有extern的情況下可能是_Z3_foo1,_Z3_foo2將導致連接錯誤,這里的extern表示在連接庫時,使用foo1,foo2函數名。
5.gcc編譯的一些知識
gcc -E hello.c -o
hello.i 只預處理
gcc -S hello.c -o
hello.s 只編譯
gcc -c -fpic first.c second.c
編譯成共享庫:-fpic選項告訴連接器使用got表定位跳轉指令,使加載器可以加載該動態庫到任何地址(具體過程可在本文后面找到)
6.gcc對c語言的擴展
void fetal_error() __attribute__(noreturn); 聲明函數:無返回值
__attribute__((noinline)) int foo1(){……}定義函數:不擴展為內聯函數
int getlim() __attribute__((pure, noinline));聲明函數:不內聯,不修改全局變量
void mspec(void) __attribute__((section(“specials”)));聲明函數:連接到特定節中
補充:除非使用-O優化級別,否則函數不會真正的內聯。
其他屬性:
函數
|
always_inline
|
|
函數
|
const
|
同pure
|
函數
|
constructor
|
加入到crt0調用的初始化函數表
|
函數
|
deprecated
|
無論何時調用函數,總是讓編譯器警告
|
函數
|
destructor
|
|
函數
|
section
|
放到命名的section中,而不是默認的.text
|
變量
|
aligned
|
分配該變量內存地址時對齊屬性,例:
int value __attribute__((aligned(32)));
|
變量
|
deprecated
|
無論何時引用變量,總是讓編譯器警告
|
變量
|
packed
|
使數據結構使用最小的空間,例如:
typedef struct zrecord{
char a;
int b __attribute((packed));
}zrecord_t;
變量b在內存中和a沒有空隙
|
變量
|
section
|
同上,例:
int trigger __attribute__((section(“domx”))) = 0;
|
類型
|
aligned
|
同上,例:
struc blockm{
char j[3];
}__attribute__((aligned(32)));
|
類型
|
deprecated
|
同上
|
類型
|
packed
|
同上
|
|
|
|
gcc內嵌函數:
void *__builtin_return_address(unsigned int level);
void *__builtin_frame_address(unsigned int leve);
以上兩個函數可以用于回溯函數棧,如果編譯器優化成noframe呢,誰愿意驗證一下?
gcc使用__asm__,
__typeof__, __inline__替代asm, typeof, inline。-std和-ansi會使后者失去功能。
標識符局部化,使用__label__標簽:
int main(……){
{
__label__ jmp1;
goto jmp1;
}
goto
jmp1;
/* 錯誤:jmp1未定義 */
}
typeof的一些技巧:
|
char *chptr
|
a char point
|
|
typeof (*chptr) ch;
|
a char
|
|
typeof (ch) *chptr2;
|
a char point
|
|
typeof(chptr) chparray[10];
|
ten char pointers
|
|
typeof(*chptr) charray[10];
|
ten char
|
|
typeof (ch) charray2[10];
|
ten chars
|
7.objdump程序
|
-a
|
|
文檔頭文件信息
|
|
-d
|
|
可執行代碼的反匯編
|
|
-D
|
|
反匯編可執行代碼及數據
|
|
-f
|
|
完整文件頭的內容
|
|
-h
|
|
section表
|
|
-p
|
|
目標格式的文件頭內容
|
調試器呢?網上的gdb教程已足夠的多,不再畫蛇添足了。
8.平臺IA32的一些知識
指令碼格式:
指令前綴(0~4字節)
|
操作碼(1~3字節)
|
可選修飾符(0~4字節)
|
可選數據元素(0~4字節)
|
指令前綴:較重要的有內存鎖定前綴(smp系統中使用)
操作碼:ia32唯一必須的部分
修飾符:使用哪些寄存器,尋址方式,SIB字節
數據元素:靜態數值或內存位置
ia32比較重要的技術:指令預取,解碼管線,分支預測,亂序執行引擎
(網絡上可以找到很多相關的文章)
通用寄存器(8個32位):eax, ebx,
ecx, edx, esi, edi, esp, ebp
端寄存器(6個16位):cs, ds, ss,
es, fs, gs
指令指針(1個32位):eip
浮點寄存器(8個80位):形成一個fpu堆棧
控制寄存器(5個32位):cr0, cr1,
cr2, cr3, cr4
較重要的是cr0:控制操作模式和處理器狀態
cr3:內存分頁表描述寄存器
調試寄存器(8個32位):
標識寄存器(1個32位):狀態,控制,系統(共使用17位):陷阱,中斷,進位,溢出等
說明:mmx使用fpu堆棧作為寄存器,sse,
sse2, sse3沒有寄存器,只提供相關的指令功能。
9.gas匯編工具:as(at&t風格)語法說明
使用$標識立即數
|
再寄存器前面加上%
|
源操作數在前,目標操作數在后
|
使用$獲取變量地址
|
長跳轉使用:ljmp $section, $offset
|
一個簡單的匯編語言程序框架:
.section .data
……
.section .bss
……
.section .text
.globl _start
_start:
……
范例:
#cpuid2.s View the CPUID Vendor ID string using C library
calls
.section .datatext
output:
.asciz "The processor Vendor ID
is '%s'\n"
.section .bss
.lcomm buffer, 12
.section .text
.globl _start
_start:
movl $0, %eax
cpuid
movl $buffer, %edi
movl %ebx, (%edi)
movl %edx, 4(%edi)
movl %ecx, 8(%edi)
pushl $buffer
pushl $output
call printf
addl $8, %esp
pushl $0
call exit
偽指令說明:
data
|
.ascii
|
定義字符串,沒有\0結束標記
|
data
|
.asciz
|
有\0結束標記
|
data
|
.byte
|
字節
|
data
|
.int
|
32位
|
data
|
.long
|
32位
|
data
|
.shot
|
16位
|
bss
|
.lcomm
|
對于上面的例子是聲明12字節的緩沖區,l標識local,僅當前匯編程序可用
|
bss
|
.comm
|
通用內存區域
|
data/text
|
.equ
|
.equ LINUX_SYS_CALL, 0x80
movl $ LINUX_SYS_CALL, %eax
說明:equ不是宏而是常量,會占據數據/代碼段空間
|
指令集說明:
|
movb/movw/movl
|
|
|
cmov
|
根據cf, of, pf, zf等標識位判斷并mov
|
|
xchg
|
操作時會lock內存,非常耗費cpu時間
|
|
bswap
|
翻轉寄存器中字節序
|
|
xadd
|
|
|
pushx, popx
|
|
|
pushad, popad
|
|
|
jmp
|
|
|
call
|
|
|
cmp
|
|
|
jz/jb/jne/jge
|
|
|
loop
|
|
|
addb/addw/addl
|
|
|
subb/subw/subl
|
|
|
dec/inc
|
|
|
mulb/muw/mull
無符號乘法
|
源操作數長度
|
目標操作數
|
目標位置
|
8位
|
al
|
ax
|
16位
|
ax
|
dx:ax
|
32位
|
eax
|
edx:eax
|
|
imul有符合乘法
|
|
|
divb/divw/divl
無符合除法
(被除數在eax中,除數在指令中給出)
|
被除數
|
被除數長
|
商
|
余數
|
ax
|
16位
|
al
|
ah
|
dx:ax
|
32位
|
ax
|
dx
|
edx:eax
|
64位
|
eax
|
edx
|
|
idiv有符合除法
|
|
|
sal/shl/sar/shr
|
移位
|
|
rol/ror/rcl/rcr
|
循環移位
|
|
leal
|
取地址:leal output, %eax
等同于:movl $output, %eax
|
|
rep
|
rep movsb 執行ecx次
|
|
lodsb/lodsw/lodsl
stosb/stosw/stosl
|
取存內存中的數據
|
|
|
|
|
|
|
|
|
gas程序范例(函數調用):
文件1:area.s定義函數area
# area.s - The areacircumference function
.section .text
.type area, @function
.globl area
area:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
fldpi
filds 8(%ebp)
fmul %st(0), %st(0)
fmulp %st(0), %st(1)
fstps -4(%ebp)
movl -4(%ebp), %eax
movl %ebp, %esp
popl %ebp
ret
文件2:functest4.s調用者
# functest4.s - An example of using external functions
.section .data
precision:
.byte 0x7f,
0x00
.section .bss
.lcomm result, 4
.section .text
.globl _start
_start:
nop
finit
fldcw precision
pushl $10
call area
addl $4, %esp
movl %eax, result
pushl $2
call area
addl $4, %esp
movl %eax, result
pushl $120
call area
addl $4, %esp
movl %eax, result
movl $1, %eax
movl $0, %ebx
int $0x80
10.讀連接器和加載器的一些筆記,感謝原作者colyli at gmail
dot com,看了他翻譯的lnl及寫的一個os,受益匪淺。
如果不是很深入的研究連接器和加載器的話,了解一些原理就足夠了。舉個例子說明吧:
1 #include <unistd.h>
2 #include <stdlib.h>
3 #include <stdio.h>
4 #include <string.h>
5
6 int a = 1;
7 int main()
8 {
9
printf("value: %d\n", a);
10
11
return 0;
12 }
編譯指令:gcc -c hello.c -o
hello.o
匯編
gcc -o hello
hello.o
編譯
objdump -d
hello.o
反匯編目標文件
objdump -d
hello
反匯編可執行文件
比較兩端結果:
objdump -d hello.o
|
objdump -d hello
|
00000000 <main>:
0: 55
push %ebp
1: 89 e5
mov %esp,%ebp
3: 83 ec 08 sub
$0x8,%esp
6: 83 e4 f0
and $0xfffffff0,%esp
9: b8 00 00 00 00 mov
$0x0,%eax
e: 83 c0
0f
add $0xf,%eax
11: 83 c0
0f
add $0xf,%eax
14: c1 e8 04
shr $0x4,%eax
17: c1 e0 04
shl $0x4,%eax
1a:
29 c4
sub %eax,%esp
1c:
83 ec 08 sub $0x8,%esp
1f:
ff 35 00 00 00 00 pushl 0x0
25: 68 00 00 00 00 push
$0x0
2a:
e8 fc ff ff ff call 2b
<main+0x2b>
2f:
83 c4 10
add $0x10,%esp
32: b8 00 00 00 00 mov $0x0,%eax
37:
c9
leave
38:
c3
ret
|
08048368 <main>:
8048368: 55 push %ebp
8048369: 89 e5 mov %esp,%ebp
804836b: 83 ec 08 sub $0x8,%esp
804836e: 83 e4 f0
and $0xfffffff0,%esp
8048371: b8 00 00 00 00 mov $0x0,%eax
8048376: 83 c0 0f
add $0xf,%eax
8048379: 83 c0 0f add
$0xf,%eax
804837c:
c1 e8 04 shr $0x4,%eax
804837f:
c1 e0 04 shl $0x4,%eax
8048382: 29 c4
sub %eax,%esp
8048384: 83 ec 08
sub $0x8,%esp
8048387: ff 35 94 95 04 08
pushl 0x8049594
804838d: 68 84 84 04 08
push $0x8048484
8048392: e8 19 ff ff ff call
80482b0
<printf@plt>
8048397: 83 c4
10 add $0x10,%esp
804839a:
b8 00 00 00 00 mov $0x0,%eax
804839f:
c9
leave
80483a0:
c3
ret
80483a1:
90
nop
80483a2:
90
nop
80483a3:
90
nop
|
簡單說明:由于程序運行時訪問內存,執行跳轉都需要確切的地址。所以匯編處理的目標文件里面沒有包含,而是把這個工作放到連接器中:即定位地址。
當程序需要動態鏈接到某個庫上時,使用該庫的got表動態定位跳轉即可。
具體可以看colyli大俠的《鏈接器和加載器Beta 2》,及《從程序員角度看ELF》
11.連接器腳本ld—script(相關內容來自《GLD中文手冊》)
ld --verbose查看默認鏈接腳本
ld把一定量的目標文件跟檔案文件連接起來,并重定位它們的數據,連接符號引用.一般在編譯一個程序時,最后一步就是運行ld。
實例1:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
注釋:“.”是定位計數器,設置當前節的地址。
實例2:
floating_point = 0;
SECTIONS
{
. = ALIGN(4);
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
. = ALIGN(4);
_bdata = (. + 3) & ~ 3;
.data : { *(.data) }
}
注釋:定義一個符合_etext,地址為.text結束的地方,注意源程序中不能在此定義該符合,否則鏈接器會提示重定義,而是應該象下面這樣使用:
extern char _etext;
但是可以在源程序中使用etext符合,連接器不導出它到目標文件。
實例3:
SECTIONS {
outputa 0x10000 :
{
all.o
foo.o (.input1)
}
outputb :
{
foo.o (.input2)
foo1.o (.input1)
}
outputc :
{
*(.input1)
*(.input2)
}
}
這個例子是一個完整的連接腳本。它告訴連接器去讀取文件all.o中的所有節,并把它們放到輸出節outputa的開始位置處, 該輸出節是從位置0x10000處開始的。從文件foo.o中來的所有節.input1在同一個輸出節中緊密排列。 從文件foo.o中來的所有節.input2全部放入到輸出節outputb中,后面跟上從foo1.o中來的節.input1。來自所有文件的所有余下的.input1和.input2節被寫入到輸出節outputc中。
示例4:連接器填充法則:
SECTIONS { .text : { *(.text) } LONG(1)
.data : { *(.data) }
}
錯誤
SECTIONS { .text : { *(.text) ;
LONG(1) } .data : { *(.data) }
} 正確
示例5:VMA和LMA不同的情況
SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; }
.mdata 0x2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = .
; }
.bss 0x3000 :
{ _bstart = . ; *(.bss)
*(COMMON) ; _bend = . ;}
}
程序:
extern char _etext, _data, _edata,
_bstart, _bend;
char *src = &_etext;
char *dst = &_data;
/* ROM has data at end of text; copy
it. */
while (dst < &_edata) {
*dst++ = *src++;
}
/* Zero bss */
for (dst = &_bstart; dst< &_bend; dst++)
*dst = 0;
示例6:linux-2.6.14/arch/i386/kernel
$ vi vmlinux.lds.S
linux內核的鏈接腳本,自行分析吧,有點復雜哦。