Linux平台Makefile文件的編寫基礎篇


目的:
       基本掌握了 make 的用法,能在Linux系統上編程。
環境:
       
Linux系統,或者有一台Linux服務器,通過終端連接。一句話:有Linux編譯環境。
准備:
       
准備三個文件:file1.c, file2.c, file2.h
       file1.c:
              #include <stdio.h>
              #include "file2.h"
              int main()
              {
                     printf("print file1$$$$$$$$$$$$$$$$$$$$$$$$\n");
                     File2Print();
                     return 0;
              }

       file2.h:

              #ifndef FILE2_H_
              #define    FILE2_H_

                      #ifdef __cplusplus

                            extern "C" {

                     #endif

                     void File2Print();

                     #ifdef __cplusplus

                            }

                     #endif

              #endif

       file2.c:
              #include "file2.h"
              void File2Print()
              {
                     printf("Print file2**********************\n");
              }

基礎:
       先來個例子:
       有這么個Makefile文件。(文件和Makefile在同一目錄)
       === makefile 開始 ===
              helloworld:file1.o file2.o
                     gcc file1.o file2.o -o helloworld

              file1.o:file1.c file2.h
                     gcc -c file1.c -o file1.o

               file2.o:file2.c file2.h

                     gcc -c file2.c -o file2.o

              clean:

                     rm -rf *.o helloworld

       === makefile 結束 ===

一個 makefile 主要含有一系列的規則,如下:
A: B
(tab)<command>
(tab)<command>

每個命令行前都必須有tab符號。

 

上面的makefile文件目的就是要編譯一個helloworld的可執行文件。讓我們一句一句來解釋:

       helloworld : file1.o file2.o                 helloworld依賴file1.o file2.o兩個目標文件。

       gcc File1.o File2.o -o helloworld:      編譯出helloworld可執行文件。-o表示你指定 的目標文件名。

      

       file1.o : file1.c    file1.o依賴file1.c文件。

       gcc -c file1.c -o file1.o                  編譯出file1.o文件。-c表示gcc 只把給它的文件編譯成目標文件, 用源碼文件的文件名命名但把其后綴由“.c”“.cc”變成“.o”。在這句中,可以省略-o file1.o,編譯器默認生成file1.o文件,這就是-c的作用。

 

              file2.o : file2.c file2.h
              gcc -c file2.c -o file2.o

這兩句和上兩句相同。

 

       clean:

              rm -rf *.o helloworld

當用戶鍵入make clean命令時,會刪除*.o helloworld文件。

 

如果要編譯cpp文件,只要把gcc改成g++就行了。

寫好Makefile文件,在命令行中直接鍵入make命令,就會執行Makefile中的內容了。

 

到這步我想你能編一個Helloworld程序了。

 

上一層樓:使用變量

       上面提到一句,如果要編譯cpp文件,只要把gcc改成g++就行了。但如果Makefile中有很多gcc,那不就很麻煩了。

       第二個例子:

       === makefile 開始 ===
              OBJS = file1.o file2.o
              CC = gcc
              CFLAGS = -Wall -O -g

              helloworld : $(OBJS)
                     $(CC) $(OBJS) -o helloworld

              file1.o : file1.c file2.h
                     $(CC) $(CFLAGS) -c file1.c -o file1.o

              file2.o : file2.c file2.h
                     $(CC) $(CFLAGS) -c file2.c -o file2.o

 

              clean:

                     rm -rf *.o helloworld
=== makefile 
結束 ===

 

       這里我們應用到了變量。要設定一個變量,你只要在一行的開始寫下這個變量的名字,后 面跟一個 = 號,后面跟你要設定的這個變量的值。以后你要引用 這個變量,寫一個 $ 符號,后面是圍在括號里的變量名。

 

CFLAGS = -Wall -O –g,解釋一下。這是配置編譯器設置,並把它賦值給CFFLAGS變量。

-Wall          輸出所有的警告信息。

-O              在編譯時進行優化。

-g               表示編譯debug版本。

 

       這樣寫的Makefile文件比較簡單,但很容易就會發現缺點,那就是要列出所有的c文件。如果你添加一個c文件,那就需要修改Makefile文件,這在項目開發中還是比較麻煩的。

 

 

再上一層樓:使用函數

       學到這里,你也許會說,這就好像編程序嗎?有變量,也有函數。其實這就是編程序,只不過用的語言不同而已。

       第三個例子:

       === makefile 開始 ===
              CC = gcc

              XX = g++
              CFLAGS = -Wall -O –g

              TARGET = ./helloworld

              %.o: %.c

                     $(CC) $(CFLAGS) -c $< -o $@

              %.o:%.cpp

                     $(XX) $(CFLAGS) -c $< -o $@

 

              SOURCES = $(wildcard *.c *.cpp)
              OBJS = $(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(SOURCES)))


              $(TARGET) : $(OBJS)
                     $(XX) $(OBJS) -o $(TARGET)

                     chmod a+x $(TARGET)

clean:

       rm -rf *.o helloworld
=== makefile 
結束 ===

函數1wildcard

       產生一個所有以 '.c' 結尾的文件的列表。

       SOURCES = $(wildcard *.c *.cpp)表示產生一個所有以 .c.cpp結尾的文件的列表,然后存入變量 SOURCES 里。

 

函數2patsubst

       匹配替換,有三個參數。第一個是一個需要匹配的式樣,第二個表示用什么來替換它,第三個是一個需要被處理的由空格分隔的列表。

OBJS = $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCES)))表示把文件列表中所有的.c,.cpp字符變成.o,形成一個新的文件列表,然后存入OBJS變量中。

 

%.o: %.c

       $(CC) $(CFLAGS) -c $< -o $@

%.o:%.cpp

       $(XX) $(CFLAGS) -c $< -o $@

       這幾句命令表示把所有的.c,.cpp編譯成.o文件。

       這里有三個比較有用的內部變量。$@ 擴展成當前規則的目的文件名, $< 擴展成依靠       列表中的第一個依靠文件,而 $^ 擴展成整個依靠的列表(除掉了里面所有重 復的文件名)。

 

       chmod a+x $(TARGET)表示把helloworld強制變成可執行文件。


LDFLAGS是選項,LIBS是要鏈接的庫。都是喂給ld的,只不過一個是告訴ld怎么吃,一個是告訴ld要吃什么。

網上不難搜索到上面這段話。不過“告訴ld怎么吃”是什么意思呢?

看看如下選項:

LDFLAGS = -L/var/xxx/lib -L/opt/mysql/lib

LIBS = -lmysqlclient -liconv

這就明白了。LDFLAGS告訴鏈接器從哪里尋找庫文件,LIBS告訴鏈接器要鏈接哪些庫文件。不過使用時鏈接階段這兩個參數都會加上,所以你即使將這兩個的值互換,也沒有問題。


說到這里,進一步說說LDFLAGS指定-L雖然能讓鏈接器找到庫進行鏈接,但是運行時鏈接器卻找不到這個庫,如果要讓軟件運行時庫文件的路徑也得到擴展,那么我們需要增加這兩個庫給"-Wl,R"

LDFLAGS = -L/var/xxx/lib -L/opt/mysql/lib -Wl,R/var/xxx/lib -Wl,R/opt/mysql/lib

如 果在執行./configure以前設置環境變量export LDFLAGS="-L/var/xxx/lib -L/opt/mysql/lib -Wl,R/var/xxx/lib -Wl,R/opt/mysql/lib" ,注意設置環境變量等號兩邊不可以有空格,而且要加上引號哦(shell的用法)。那么執行configure以后,Makefile將會設置這個選項, 鏈接時會有這個參數,編譯出來的可執行程序的庫文件搜索路徑就得到擴展了。


------------------------------------------------------------------------------------------------------------------------


PS:-Wl,R在GraphicsMagick環境下,用為-R, 也就是LDFLAGS = -L/var/xxx/lib -R/var/xxx/lib



 

CFLAGS 或 CPPFLAGS的用法
CPPFLAGS='-I/usr/local/libjpeg/include -I/usr/local/libpng/include'


arm-linux-ld命令 ld鏈接腳本

我們對每個c或者匯編文件進行單獨編譯,但是不去連接,生成很多.o 的文件,這些.o文件首先是分散的,我們首先要考慮的如何組合起來;其次,這些.o文件存在相互調用的關系;再者,我們最后生成的bin文件是要在硬件中運行的,每一部分放在什么地址都要有仔細的說明。我覺得在寫makefile的時候,最為重要的就是ld的理解,下面說說我的經驗:

 

首先,要確定我們的程序用沒有用到標准的c庫,或者一些系統的庫文件,這些一般是在操作系統之上開發要注意的問題,這里並不多說,熟悉在Linux編程的人,基本上都會用ld命令;這里,我們從頭開始,直接進行匯編語言的連接。

 

我們寫一個匯編程序,控制GPIO,從而控制外接的LED,代碼如下;

.text

.global _start

_start:

    LDR R0,=0x56000010 @GPBCON寄存器
   
    MOV R1,# 0x00000400
    str R1,[R0]
   
    LDR R0,=0x56000014
    MOV R1,#0x00000000
   
    STR R1,[R0]
   
    MAIN_LOOP:
            B MAIN_LOOP

    代碼很簡單,就是一個對io口進行設置然后寫數據。我們看它是如何編譯的,注意我們這里使用的不是arm-linux-gcc而是arm-elf-gcc,二者之間沒有什么比較大的區別,arm-linux-gcc可能包含更多的庫文件,在命令行的編譯上面是沒有區別。我們來看是如何編譯的:

       arm-elf-gcc -g -c -o led_On.o led_On.s 首先純編譯不連接

       arm-elf-ld -Ttext 0x00000000 -g led_On.o -o led_on_elf

       用Ttext指明我們程序存儲的地方這里生成的是elf文件,還不是我們真正的bin,但是可以借助一些工具可以進行調試。然后:

       arm-elf-objcopy -O binary -S led_on_elf led_on.bin  

生成bin文件。

 

-T選項是ld命令中比較重要的一個選項,可以用它直接指明代碼的代碼段、數據段、bss段,對於復雜的連接,可以專門寫一個腳本來告訴編譯器如何連接。

    -Ttext   addr

    -Tdata   addr

    -Tbss    addr

arm-elf-ld -Ttext 0x00000000 -g led_On.o -o led_on_elf ,運行地址為0x00000000,由於沒有指明數據段和bss,他們會默認的依次放在后面。相同的代碼不同的Ttext,你可以對比一下他們之間會變的差異,ld會自動調整跳轉的地址。

 

 

     第二個概念:section,section可以理解成→塊,例如像c里面的一個子函數,就是一個section,鏈接器ld把object文件中的每個section都作為一個整體,為其分配運行的地址(memory layout),這個過程就是重定位(relocation);最后把所有目標文件合並為一個目標文件

 

     鏈接通過一個linker script來控制,這個腳本描述了輸入文件的sections輸出文件的映射,以及輸出文件的memory layout。

    因此,linker總會使用一個linker script,如果不特別指定,則使用默認的script;可以使用‘-T’命令行選項來指定一個linker script。

   *映像文件的輸入段與輸出段

    linker把多個輸入文件合並為一個輸出文件。輸出文件和輸入文件都是目標文件(object file),輸出文件通常被稱為可執行文件(executable)。

    每個目標文件都有一系列section,輸入文件的section稱為input section,輸出文件的section則稱為output section。

     一個section可以是loadable的,即輸出文件運行時需要將這樣的section加載到memory(類似於RO&RW段);也可以是 allocatable的,這樣的section沒有任何內容,某些時候用0對相應的memory區域進行初始化(類似於ZI段);如果一個 section既非loadable也非allocatable,則它通常包含的是調試信息

    每個loadable或 allocatable的output section都有兩個地址,一是VMA(virtual memory address),是該section的運行時域地址;二是LMA(load memory address),是該section的加載時域地址

   可以通過objdump工具附加'-h'選項來查看目標文件中的sections。

 

*簡單的Linker script

(1) SECTIONS命令:

The SECTIONS command tells the linker how to map input sections into output sections, and how to place the output sections in memory.

命令格式如下:

SECTIONS

{

sections-command

sections-command

......

}

其中sections-command可以是ENTRY命令,符號賦值,輸出段描述,也可以是overlay描述。

(2) 地址計數器‘.’(location counter):

該符號只能用於SECTIONS命令內部,初始值為‘0’,可以對該符號進行賦值,也可以使用該符號進行計算或賦值給其他符號。它會自動根據SECTIONS命令內部所描述的輸出段的大小來計算當前的地址。

(3) 輸出段描述(output section description):

前面提到在SECTIONS命令中可以作輸出段描述,描述的格式如下:

section [address] [(type)] : [AT(lma)]

{

output-section-command

output-section-command

...

} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

很多附加選項是用不到的。其中的output-section-command又可以是符號賦值,輸入段描述,要直接包含的數據值,或者某一特定的輸出段關鍵字。


*linker script 實例

==============================

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS {

    . = 0xa3f00000;

    __boot_start = .;

    .start ALIGN(4) : {

        *(.text.start)

    }


    .setup ALIGN(4) : {

        setup_block = .;

        *(.setup)

        setup_block_end = .;

    }


    .text ALIGN(4) : {

        *(.text)

    }


    .rodata ALIGN(4) : {

        *(.rodata)

    }

    .data ALIGN(4) : {

        *(.data)

    }


    .got ALIGN(4) : {

        *(.got)

    }

    __boot_end = .;


    .bss ALIGN(16) : {

        bss_start = .;

        *(.bss)

        *(COMMON)

        bss_end = .;

    }


    .comment ALIGN(16) : {

        *(.comment)

    }

    stack_point = __boot_start + 0x00100000;

    loader_size = __boot_end - __boot_start;

    setup_size = setup_block_end - setup_block;

}

=============================

在SECTIONS命令中的類似於下面的描述結構就是輸出段描述:

.start ALIGN(4) : {

    *(.text.start)

}

.start 為output section name,ALIGN(4)返回一個基於location counter(.)的4字節對齊的地址值。*(.text.start)是輸入段描述,*為通配符,意思是把所有被鏈接的object文件中的.text.start段都鏈接進這個名為.start的輸出段。

源文件中所標識的section及其屬性實際上就是對輸入段的描述,例如.text.start輸入段在源文件start.S中的代碼如下:

.section .text.start

.global _start

_start :

    b start

arm-elf-ld -Ttimer.lds -o timer_elf header .o

這里就必須存在一個timer.lds的文件。

對於.lds文件,它定義了整個程序編譯之后的連接過程,決定了一個可執行程序的各個段的存儲位置。雖然現在我還沒怎么用它,但感覺還是挺重要的,有必要了解一下。

先看一下GNU官方網站上對.lds文件形式的完整描述:

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}

secname和contents是必須的,其他的都是可選的。下面挑幾個常用的看看:

1、secname:段名

2、contents:決定哪些內容放在本段,可以是整個目標文件,也可以是目標文件中的某段(代碼段、數據段等)

3、start:本段連接(運行)的地址,如果沒有使用AT(ldadr),本段存儲的地址也是start。GNU網站上說start可以用任意一種描述地址的符號來描述。

4、AT(ldadr):定義本段存儲(加載)的地址。


SECTIONS {
firtst 0x00000000 : { head.o init.o }
second 0x30000000 : AT(4096) { main.o }
}

    以上,head.o放在0x00000000地址開始處,init.o放在head.o后面,他們的運行地址也是0x00000000,即連接和存儲地址相同(沒有AT指定)

     main.o放在4096(0x1000,是AT指定的,存儲地址)開始處,但是它的運行地址在0x30000000,運行之前需要從0x1000(加載處)復制到0x30000000(運行處),此過程也就用到了讀取Nand flash。

     這就是存儲地址和連接(運行)地址的不同,稱為加載時域運行時域,可以在.lds連接腳本文件中分別指定。

編寫好的.lds文件,在用arm-linux-ld連接命令時帶-Tfilename來調用執行,如
arm-linux-ld –Tnand.lds x.o y.o –o xy.o。也用-Ttext參數直接指定連接地址,如
arm-linux-ld –Ttext 0x30000000 x.o y.o –o xy.o。

 

**************************************************************************************************

    既然程序有了兩種地址,就涉及到一些跳轉指令的區別,這里正好寫下來,以后萬一忘記了也可查看,以前不少東西沒記下來現在忘得差不多了。

      ARM匯編中,常有兩種跳轉方法b跳轉指令(位置無關指令)ldr指令(位置相關指令) 向PC賦值。

 

我自己經過歸納如下:

     b step1 :b跳轉指令是相對跳轉依賴當前PC的值,偏移量是通過該指令本身的bit[23:0]算出來的,這使得使用b指令的程序不依賴於要跳到的代碼的位置,只看指令本身。

 

     ldr pc, =step1 :該指令是從內存中的某個位置(step1)讀出數據並賦給PC同樣依賴當前PC的值,但是偏移量是那個位置(step1)的連接地址(運行時的地址),所以可以用它實現從Flash到RAM的程序跳轉。

 

    此外,有必要回味一下adr偽指令,U-boot中那段relocate代碼就是通過adr實現當前程序是在RAM中還是flash中。仍然用我當時的注釋

adr r0, _start

ldr r1, _TEXT_BASE

    cmp r0, r1

   下面,結合u-boot.lds看看一個正式的連接腳本文件。這個文件的基本功能還能看明白,雖然上面分析了好多,但其中那些GNU風格的符號還是着實讓我感到迷惑。

OUTPUT_FORMAT("elf32­littlearm", "elf32­littlearm", "elf32­littlearm")
;指定輸出可執行文件是elf格式,32位ARM指令,小端
OUTPUT_ARCH(arm)
;指定輸出可執行文件的平台為ARM
ENTRY(_start)
;指定輸出可執行文件的起始代碼段為_start.
SECTIONS
{
        . = 0x00000000 ; 從0x0位置開始
        . = ALIGN(4) ; 代碼以4字節對齊
        .text : ;指定代碼段
        {
          cpu/arm920t/start.o (.text) ; 代碼的第一個代碼部分
          *(.text) ;其它代碼部分
        }
        . = ALIGN(4)
        .rodata : { *(.rodata) } ;指定只讀數據段
        . = ALIGN(4);
        .data : { *(.data) } ;指定讀/寫數據段
        . = ALIGN(4);
        .got : { *(.got) } ;指定got段, got段式是uboot自定義的一個段, 非標准段
        __u_boot_cmd_start = . ;把__u_boot_cmd_start賦值為當前位置, 即起始位置
        .u_boot_cmd : { *(.u_boot_cmd) } ;指定u_boot_cmd段, uboot把所有的uboot命令放在該段.
        __u_boot_cmd_end = .;把__u_boot_cmd_end賦值為當前位置,即結束位置
        . = ALIGN(4);
        __bss_start = .; 把__bss_start賦值為當前位置,即bss段的開始位置
        .bss : { *(.bss) }; 指定bss段
        _end = .; 把_end賦值為當前位置,即bss段的結束位置

轉自 御劍踏浪


arm-linux-gcc和arm-linux-ld、arm-linux-objcopy之間的關系

arm-linux-ld 是連接器,它把一些目標和歸檔文件結合在一起,重定位數據,並連接符號引用。通常,建立一個新編譯程序的最后一步就是調用ld。

arm-linux-gcc -wall -O2 -c -o $@ $<
-o 只激活預處理,編譯,和匯編,也就是他只把程序做成obj文件 
-Wall 指定產生全部的警告信息 
-O2 編譯器對程序提供的編譯優化選項,在編譯的時候使用該選項,可以使生成的執行文件的執行效率提高 
-c 表示只要求編譯器進行編譯,而不要進行鏈接,生成以源文件的文件名命名但把其后綴由 .c 或 .cc 變成 .o 的目標文件 
-S 只激活預處理和編譯,就是指把文件編譯成為匯編代碼
arm-linux-ld 直接指定代碼段,數據段,BSS段的起始地址
-Tbss ADDRESS Set address of .bss section

-Tdata ADDRESS Set address of .data section

-Ttext ADDRESS Set address of .text section
示例:
${CROSS}ld -Ttext=0x33000000 led.o -o led.elf
使用連接腳本設置地址:
arm-linux-ld -Tbeep.lds start.o beep.o -o beep.elf
其中beep.lds 為連接腳本如下:
arm-linux-objcopy被用來復制一個目標文件的內容到另一個文件中,可用於不同源文件的之間的格式轉換
示例:
arm-linux-objcopy –o binary –S elf_file bin_file
常用的選項:
input-file , outflie
輸入和輸出文件,如果沒有outfile,則輸出文件名為輸入文件名
2.-l bfdname或—input-target=bfdname
用來指明源文件的格式,bfdname是BFD庫中描述的標准格式名,如果沒指明,則arm-linux-objcopy自己分析
3.-O bfdname 輸出的格式
4.-F bfdname 同時指明源文件,目的文件的格式
5.-R sectionname 從輸出文件中刪除掉所有名為sectionname的段
6.-S 不從源文件中復制重定位信息和符號信息到目標文件中
7.-g 不從源文件中復制調試符號到目標文件中
arm-linux-objdump
查看目標文件(.o文件)和庫文件(.a文件)信息
arm-linux-objdump -D -m arm beep.elf > beep.dis
-D 顯示文件中所有匯編信息
-m machine

指定反匯編目標文件時使用的架構,當待反匯編文件本身沒有描述架構信息的時候(比如S-records),這個選項很有用。可以用-i選項列出這里能夠指定的架構.
[guowenxue@localhost asm_c_buzzer]$ cat beep.lds 
/***********************************************************************
* File: beep.lds
* Version: 1.0.0
* Copyright: 2011 (c) Guo Wenxue <guowenxue@gmail.com>
* Description: Cross tool link text, refer to u-boot.lds
* ChangeLog: 1, Release initial version on "Mon Mar 21 21:09:52 CST 2011"
*
**********************************************************************/
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS{
. = 0x33000000;
.text : {

*(.text)
*(.rodata)
}

.data ALIGN(4): {

*(.data)

}
.bss ALIGN(4): {

*(.bss)

}

}

[guowenxue@localhost asm_c_buzzer]$ cat makefile 

# ***********************************************************************

# * File: makefile

# * Version: 1.0.0

# * Copyright: 2011 (c) Guo Wenxue <guowenxue@gmail.com>

# * Description: Makefile used to cross compile the ASM and C source code

# * ChangeLog: 1, Release initial version on "Mon Mar 21 21:09:52 CST 2011"

# *
# ***********************************************************************
CROSS = /opt/buildroot-2011.02/arm920t/usr/bin/arm-linux-
CFLAGS = 
beep.bin: start.S beep.c
arm-linux-gcc $(CFLAGS) -c -o start.o start.S

arm-linux-gcc $(CFLAGS) -c -o beep.o beep.c

arm-linux-ld -Tbeep.lds start.o beep.o -o beep.elf

arm-linux-objcopy -O binary -S beep.elf beep.bin

rm -f *.elf *.o
install:
cp beep.bin ~/winxp -f --reply=yes
clean:
rm -f *.elf *.o
rm -f beep.bin






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM