2010年2月25日 星期四

virtual Box如何使用虛擬dos網路共享資料夾


圖五

圖四

圖三

圖二

圖一
virtual Box如何使用虛擬dos網路共享資料夾
首先參考以下連結:MS-Client 網路開機片
以下會有幾點主題說明:
一 virtualBox網路配接卡設定(virtualbox為3.0.1.2版)由於舊版本設定方式不同,因此列出版本號碼。
二 dos網路通訊協定設定
三 網路芳鄰相關設定
一 .
1先在virtualbox偏好設定/網路選項中新增一Host-only networking,如圖一。
2在虛擬dos的設定值中,將網路設定如圖二。
3至控制台設定橋接區域連線VirtualBox Host-Only Network,如圖三;並在網路橋接器新增NWLink IPX/SPX/NetBIOS Compatible Transport Protocal通訊協定。
4下載msclient.zip將檔案解壓縮。由於virtualbox可以透過實體floppy及img檔兩種方式來啟動。而現在的PC及NB都已經不再配備FLOPPY,因此在此介紹使用WinImage來管理img檔。
WinImage畫面如圖四。將msclient.zip解壓縮的檔案存到img檔去。另外也可將ms-dos6.22安裝到虛擬硬碟,詳細資料請參考http://hengch.blog.163.com/blog/static/107800672009046575193/
http://hengch.blog.163.com/blog/static/1078006720090595020576/

二.
架構 Protocol.ini檔會呼叫 Protman.dos、Protman.exe、DIS_pkt.dos 等檔案,以及 NIC 驅動程式檔案。架構您的 NDIS 驅動程式牽涉到編輯 Protocol.ini,以針對將使用開機磁片的電腦,增加特有的項目。
由於此次範例採用的是IPX通訊協定,因此若要使用TCP協定可參考此連結:http://help.netop.com/support/configuration/DOS_Host_on_TCP_IP.htm
接下來修改以下檔案:紅字表示修改過,以下檔案針對ms-client所修正,因此必須去下載virtual Box所虛擬出來的實體網卡Intel® PRO/1000 MT Desktop Adapter for DOS驅動程式

system.ini:
[network]
filesharing=no 這個改了yes沒用。
printsharing=no
autologon=yes
computername=benson
lanroot=A:\NET
username=Administrator
workgroup=WORKGROUP
reconnect=no
dospophotkey=N
lmlogon=0
logondomain=
preferredredir=full
autostart=full
maxconnections=8

[network drivers]
netcard=E1000.DOS
transport=ndishlp.sys
devdir=A:\NET
LoadRMDrivers=yes

[Password Lists]
*Shares=a:\net\Share000.PWL
ADMINISTRATOR=A:\NET\ADMINIST.PWL
BENSON=A:\NET\BENSON.PWL


protocol.ini:
[network.setup]
version=0x3110
netcard=ms$ne2clone,1,MS$NE2CLONE,1
transport=ms$ndishlp,MS$NDISHLP
transport=ms$nwlink,MS$NWLINK
lana0=ms$ne2clone,1,ms$nwlink
lana1=ms$ne2clone,1,ms$ndishlp
[ms$ne2clone]
DRIVERNAME = E1000$
; INTERRUPT=3
; IOBASE=0x300
; SlotNumber=1
[protman]
drivername=PROTMAN$
PRIORITY=MS$NDISHLP
[MS$NDISHLP]
drivername=ndishlp$
BINDINGS=ms$ne2clone
[ms$nwlink]
drivername=nwlink$
FRAME=Ethernet_802.2
BINDINGS=ms$ne2clone
LANABASE=0

net.cfg:
Link Support
Buffers 8 1514
Link Driver E1000ODI
; Remove the semi-colon in front of the frame type you will use.
; This frame type must match what the NetWare server is using.
FRAME Ethernet_802.2
; FRAME Ethernet_802.3
; FRAME Ethernet_SNAP
; FRAME Ethernet_II

NetWare DOS Requester
FIRST NETWORK DRIVE = F

; If you know the name of your preferred server, remove the
; semi-colon below and enter the preferred server name after
; the equal sign.
;preferred server =

完成後可開始使用virtualbox開啟虛擬dos,畫面如圖五。
由於這樣實作的dos形式上就如同Guest身份,因此無法在dos共享資料夾給windows使用。

2010年2月4日 星期四

從以下幾個方面地分析u-boot並移植到FS2410板上








寫的太棒了
本文從以下幾個方面粗淺地分析u-boot並移植到FS2410板上:
1、u-boot工程的總體結構
2、u-boot的流程、主要的資料結構、記憶體分配。

3、u-boot的重要細節,主要分析流程中各函數的功能。
4、基於FS2410板子的u-boot移植。實現了NOR Flash和NAND Flash啟動,網路功能。 
這些認識源於自己移植u-boot過程中查找的資料和對源碼的簡單閱讀。下面主要以smdk2410為分析對象。

一、u-boot工程的總體結構:
1、原始程式碼組織
對於ARM而言,主要的目錄如下:
board 平臺依賴  存放電路板相關的目錄檔,每一套板子對 應一個目錄。


如smdk2410(arm920t)
cpu 平臺依賴 存放CPU相關的目錄檔,每一款CPU對應一個目錄,例如:arm920t、 xscale、i386等目錄
lib_arm 平臺依賴 存放對ARM體系結構通用的檔,主要用於實現ARM平臺通用的函數,如軟體浮點。
common 通用 通用的多功能函數實現,如環境,命令,控制台相關的函數實現。
include 通用 標頭檔和開發板設定檔,所有開發板的設定檔都在configs目錄下
lib_generic 通用 通用庫函數的實現
net 通用 存放網路通訊協定的程式
drivers 通用 通用的設備驅動程式,主要有乙太網介面的驅動,nand驅動。
.......
2.makefile簡要分析
所有這些目錄的編譯連接都是由頂層目錄的makefile來確定的。
在執行make之前,先要執行make $(board)_config 對工程進行配置,以確定特定於目標板的各個子目錄和標頭檔。
$(board)_config:是makefile 中的一個偽目標,它傳入指定的CPU,ARCH,BOARD,SOC參數去執行mkconfig腳本。
這個腳本的主要功能在於連接目標板平臺相關的標頭檔夾,生成config.h檔包含板子的配置標頭檔。
使得makefile能根據目標板的這些參數去編譯正確的平臺相關的子目錄。
以smdk2410板為例,執行 make smdk2410_config,
主要完成三個功能:
@在include資料夾下建立相應的文件(夾)軟連接,


#如果是ARM體系將執行以下操作:
#ln -s asm-arm asm
#ln -s arch-s3c24x0 asm-arm/arch
#ln -s proc-armv asm-arm/proc

@生成Makefile包含檔include/config.mk,內容很簡單,定義了四個變數:
ARCH = arm
CPU = arm920t
BOARD = smdk2410
SOC = s3c24x0

@生成include/config.h標頭檔,只有一行:
/* Automatically generated - do not edit */
#include "config/smdk2410.h"

頂層makefile先調用各子目錄的makefile,生成目的檔案或者目的檔案庫。
然後再連接所有目的檔案(庫)生成最終的u-boot.bin。
連接的主要目標(庫)如下:
OBJS = cpu/$(CPU)/start.o
LIBS = lib_generic/libgeneric.a
LIBS += board/$(BOARDDIR)/lib$(BOARD).a
LIBS += cpu/$(CPU)/lib$(CPU).a
ifdef SOC
LIBS += cpu/$(CPU)/$(SOC)/lib$(SOC).a
endif
LIBS += lib_$(ARCH)/lib$(ARCH).a
LIBS += fs/cramfs/libcramfs.a fs/fat/libfat.a fs/fdos/libfdos.a fs/jffs2/libjffs2.a \
fs/reiserfs/libreiserfs.a fs/ext2/libext2fs.a
LIBS += net/libnet.a
LIBS += disk/libdisk.a
LIBS += rtc/librtc.a
LIBS += dtt/libdtt.a
LIBS += drivers/libdrivers.a
LIBS += drivers/nand/libnand.a
LIBS += drivers/nand_legacy/libnand_legacy.a
LIBS += drivers/sk98lin/libsk98lin.a
LIBS += post/libpost.a post/cpu/libcpu.a
LIBS += common/libcommon.a
LIBS += $(BOARDLIBS)
顯然跟平臺相關的主要是:
cpu/$(CPU)/start.o
board/$(BOARDDIR)/lib$(BOARD).a 
cpu/$(CPU)/lib$(CPU).a
cpu/$(CPU)/$(SOC)/lib$(SOC).a 
lib_$(ARCH)/lib$(ARCH).a
這裡面的四個變數定義在include/config.mk(見上述)。
其餘的均與平臺無關。
所以考慮移植的時候也主要考慮這幾個目的檔案(庫)對應的目錄。
關於u-boot 的makefile更詳細的分析可以參照http://blog.mcuol.com/User/lvembededsys/Article/4355_1.htm

3、u-boot的通用目錄是怎麼做到與平臺無關的?
include/config/smdk2410.h
這個頭檔中主要定義了兩類變數。
 一類是選項,首碼是CONFIG_,用來選擇處理器、設備介面、命令、屬性等,主要用來 決定是否編譯某些檔或者函數。

另一類是參數,首碼是CFG_,用來定義匯流排頻率、串口串列傳輸速率、Flash位址等參數。這些常數參量主要用來支援通用目錄中的代碼,定義板子資源參數。

這兩類巨集定義對u-boot的移植性非常關鍵,比如drive/CS8900.c,對cs8900而言,很多操作都是通用的,但不是所有的板子上面都有這個晶片,即使有它在記憶體中映射的基底位址也是平臺相關的。所以對於smdk2410板,在smdk2410.h中定義了
#define CONFIG_DRIVER_CS8900 1 /* we have a CS8900 on-board */
#define CS8900_BASE 0x19000300 /*IO mode base address*/
CONFIG_DRIVER_CS8900的定義使得cs8900.c可以被編譯(當然還得定義CFG_CMD_NET才行),因為cs8900.c中在函式定義的前面就有編譯條件判斷:#ifdef CONFIG_DRIVER_CS8900 如果這個選項沒有定義,整個cs8900.c就不會被編譯了。
而常數參量CS8900_BASE則用在cs8900.h標頭檔中定義各個功能寄存器的位址。u-boot的CS8900工作在IO模式下,只要給定IO寄存器在記憶體中映射的基底位址,其餘代碼就與平臺無關了。

u-boot的命令也是通過目標板的配置標頭檔來配置的,比如要添加ping命令,就必須添加CFG_CMD_NET和CFG_CMD_PING才行。不然common/cmd_net.c就不會被編譯了。
從這裡我可以這麼認為,u-boot工程可配置性和移植性可以分為兩層:
一是由makefile來實現,配置工程要包含的檔和資料夾上,用什麼編譯器。
二是由目標板的配置標頭檔來實現源碼級的可配置性,通用性。主要使用的是#ifdef #else #endif 之類來實現的。
4、smkd2410其餘重要的文件:
include/s3c24x0.h   定義了s3x24x0晶片的各個特殊功能寄存器(SFR)的位址。
cpu/arm920t/start.s 在flash中執行的引導代碼,也就是bootloader中的stage1,負責初始化硬體環境,把u-boot從flash載入到RAM中去,然後跳到lib_arm/board.c中的start_armboot中去執行。
lib_arm/board.c   u-boot的初始化流程,尤其是u-boot用到的全域資料結構gd,bd的初始化,以及設備和控制台的初始化。
board/smdk2410/flash.c 在board目錄下代碼的都是嚴重依賴目標板,對於不同的CPU,SOC,ARCH,u-boot都有相對通用的代碼,但是板子構成卻是多樣的,主要是記憶體位址,flash型號,週邊晶片如網路。對fs2410來說,主要考慮從smdk2410板來移植,差別主要在nor flash上面。

二、u-boot的流程、主要的資料結構、記憶體分配
1、u-boot的啟動流程:
從檔層面上看主要流程是在兩個檔中:cpu/arm920t/start.s,lib_arm/board.c,1)start.s 
在flash中執行的引導代碼,也就是bootloader中的stage1,負責初始化硬體環境,把u-boot從flash載入到RAM中去,然後跳到lib_arm/board.c中的start_armboot中去執行。
1.1.6版本的start.s流程:
硬體環境初始化:
1進入svc模式;2關閉watch dog;3遮罩所有IRQ遮罩;4設置時鐘頻率FCLK、HCLK、PCLK;


5清I/D cache;6禁止MMU和CACHE;7配置memory control(這個動作在lowlevel_init.S);


注意5.6.7三個步驟就是cpu_init_crit所作的事。
8重定位
如果當前代碼不在連接指定的位址上(對smdk2410是0x3f000000)則需要把u-boot從當前位置拷貝到RAM指定位置中;建立堆疊,堆疊是進入C函數前必須初始化的。
清.bss區。跳到start_armboot函數中執行。(lib_arm/board.c)
2)lib_arm/board.c:
start_armboot是U-Boot執行的第一個C語言函數,完成系統初始化工作,進入主迴圈,處理用戶輸入的命令。這裡只簡要列出了主要執行的函數流程:
void start_armboot (void)
{
//全域資料變數指標gd佔用r8。
DECLARE_GLOBAL_DATA_PTR;

/* 給全域資料變數gd安排空間*/
gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));
memset ((void*)gd, 0, sizeof (gd_t));

/* 給板子資料變數gd->bd安排空間*/
gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
memset (gd->bd, 0, sizeof (bd_t));
monitor_flash_len = _bss_start - _armboot_start;//取u-boot的長度。

/* 循序執行init_sequence陣列中的初始化函數 */
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}

/*配置可用的Flash */
size = flash_init ();
 ……
/* 初始化堆空間 */
mem_malloc_init (_armboot_start - CFG_MALLOC_LEN);
/* 重新定位環境變數, */
env_relocate ();
/* 從環境變數中獲取IP位址 */
gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr");
/* 乙太網介面MAC 位址 */
……
devices_init (); /* 設備初始化 */
jumptable_init (); //跳轉表初始化
console_init_r (); /* 完整地初始化控制台設備 */
enable_interrupts (); /* 使能中斷處理 */
/* 通過環境變數初始化 */
if ((s = getenv ("loadaddr")) != NULL) {
load_addr = simple_strtoul (s, NULL, 16);
}
/* main_loop()迴圈不斷執行 */
for (;;) {
main_loop (); /* 主迴圈函數處理執行使用者命令 -- common/main.c */
}
}

初始化函數序列init_sequence[]
init_sequence[]陣列保存著基本的初始化函數指標。這些函數名稱和實現的程式檔在下列注釋中。

init_fnc_t *init_sequence[] = {
cpu_init, /* 基本的處理器相關配置 -- cpu/arm920t/cpu.c */
board_init, /* 基本的板級相關配置 -- board/smdk2410/smdk2410.c */
interrupt_init, /* 初始化例外處理 -- cpu/arm920t/s3c24x0/interrupt.c */
env_init, /* 初始化環境變數 -- common/env_flash.c */
init_baudrate, /* 初始化串列傳輸速率設置 -- lib_arm/board.c */
serial_init, /* 串口通訊設置 -- cpu/arm920t/s3c24x0/serial.c */
console_init_f, /* 控制台初始化階段1 -- common/console.c */
display_banner, /* 列印u-boot資訊 -- lib_arm/board.c */
dram_init, /* 配置可用的RAM -- board/smdk2410/smdk2410.c */
display_dram_config, /* 顯示RAM的配置大小 -- lib_arm/board.c */
NULL,
};
整個u-boot的執行就進入等待用戶輸入命令,解析並執行命令的閉環中。
2、u-boot主要的資料結構
u-boot的主要功能是用於引導OS的,但是本身也提供許多強大的功能,可以通過輸入命令列來完成許多操作。所以它本身也是一個很完備的系統。u-boot的大部分操作都是圍繞它自身的資料結構,這些資料結構是通用的,但是不同的板子初始化這些資料就不一樣了。所以u-boot的通用代碼是依賴於這些重要的資料結構的。這裡說的資料結構其實就是一些全域變數。
1)gd 全域資料變數指標,它保存了u-boot運行需要的全域資料,類型定義:
typedef struct global_data {
bd_t *bd; //board data pointor板子數據指標
unsigned long flags;  //指示標誌,如設備已經初始化標誌等。
unsigned long baudrate; //串口串列傳輸速率
unsigned long have_console; /* 串口初始化標誌*/
unsigned long reloc_off; /* 重定位偏移,就是實際定向的位置與編譯連接時指定的位置之差,一般為0 */
unsigned long env_addr; /* 環境參數地址*/
unsigned long env_valid; /* 環境參數CRC檢驗有效標誌 */
unsigned long fb_base; /* base address of frame buffer */
 #ifdef CONFIG_VFD
unsigned char vfd_type; /* display type */
 #endif
void **jt; /* 跳轉表,1.1.6中用來函式呼叫地址登記 */
} gd_t;
2)bd 板子數據指標。板子很多重要的參數。 類型定義如下:
typedef struct bd_info {
int bi_baudrate; /* 串口串列傳輸速率 */
unsigned long bi_ip_addr; /* IP 地址 */
unsigned char bi_enetaddr[6]; /* MAC地址*/
struct environment_s *bi_env;
ulong bi_arch_number; /* unique id for this board */
ulong bi_boot_params; /* 啟動參數 */
struct /* RAM 配置 */
{
ulong start;
ulong size;
}bi_dram[CONFIG_NR_DRAM_BANKS];
} bd_t;
3)環境變數指標 env_t *env_ptr = (env_t *)(&environment[0]);(common/env_flash.c)
env_ptr指向環境參數區,系統啟動時預設的環境參數environment[],定義在common/environment.c中。 
參數解釋:
bootdelay 定義執行自動啟動的等候秒數
baudrate 定義串口控制台的串列傳輸速率
netmask 定義乙太網介面的遮罩
ethaddr 定義乙太網介面的MAC位址
bootfile 定義缺省的下載檔案
bootargs 定義傳遞給Linux內核的命令列參數
bootcmd 定義自動啟動時執行的幾條命令
serverip 定義tftp伺服器端的IP地址
ipaddr 定義本地的IP地址
stdin 定義標準輸入裝置,一般是串口
stdout 定義標準輸出設備,一般是串口
stderr 定義標準出錯資訊輸出設備,一般是串口
4)設備相關:
標準IO設備陣列evice_t *stdio_devices[] = { NULL, NULL, NULL };
設備清單    list_t devlist = 0;
device_t的定義:include\devices.h中:
typedef struct {
int flags;       /* Device flags: input/output/system */
int ext;      /* Supported extensions */
char name[16];      /* Device name */
/* GENERAL functions */
int (*start) (void);    /* To start the device */
int (*stop) (void);     /* To stop the device */
/* 輸出函數 */
void (*putc) (const char c); /* To put a char */
void (*puts) (const char *s); /* To put a string (accelerator) */
/* 輸入函數 */
int (*tstc) (void);     /* To test if a char is ready... */
int (*getc) (void);     /* To get that char */
/* Other functions */
void *priv;        /* Private extensions */
} device_t;
 u-boot把可以用為控制台輸入輸出的設備添加到設備清單devlist,並把當前用作標準IO的設備指標加入stdio_devices陣列中。
 在調用標準IO函數如printf()時將調用stdio_devices陣列對應設備的IO函數如putc()。
5)命令相關的資料結構,後面介紹。
6)與具體設備有關的資料結構,
 如flash_info_t flash_info[CFG_MAX_FLASH_BANKS];記錄nor flash的資訊。
 nand_info_t nand_info[CFG_MAX_NAND_DEVICE]; nand flash塊設備資訊
3、u-boot重定位後的記憶體分佈:
   對於smdk2410,RAM範圍從0x30000000~0x34000000. u-boot佔用高端記憶體區。



從高位址到低位址記憶體分配如下:
顯示緩衝區 (.bss_end~34000000)
u-boot(bss,data,text) (33f00000~.bss_end)
heap(for malloc)
gd(global data)
bd(board data)
stack
....
nor flash (0~2M)


三、u-boot的重要細節。
主要分析流程中各函數的功能。按啟動順序羅列一下啟動函數執行細節。按照函數start_armboot流程進行分析:
1)DECLARE_GLOBAL_DATA_PTR;
這個巨集定義在include/global_data.h中:
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
聲明一個寄存器變數 gd 佔用r8。這個巨集在所有需要引用全域資料指標gd_t *gd的源碼中都有申明。
這個申明也避免編譯器把r8分配給其它的變數. 所以gd就是r8,這個指標變數不佔用記憶體。
2)gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));
對全域資料區進行位址分配,_armboot_start為0x3f000000,CFG_MALLOC_LEN是堆大小+環境資料區大小,config/smdk2410.h中CFG_MALLOC_LEN大小定義為192KB.
3)gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
分配板子資料區bd首位址。
這樣結合start.s中棧的分配,
stack_setup:
ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */
sub r0, r0, #CFG_MALLOC_LEN /* malloc area */
sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfoCFG_GBL_DATA_SIZE =128B */
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 /* leave 3 words for abort-stack */
不難得出上文所述的記憶體分配結構。
下面幾個函數是初始化序列表init_sequence[]中的函數:
4)cpu_init();定義於cpu/arm920t/cpu.c
分配IRQ,FIQ棧底位址,由於沒有定義CONFIG_USE_IRQ,所以相當於空實現。
5)board_init;極級初始化,定義於board/smdk2410/smdk2410.c
設置PLL時鐘,GPIO,使能I/D cache.
設置bd資訊:gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;//板子的ID,沒啥意義。
gd->bd->bi_boot_params = 0x30000100;//內核啟動參數存放地址
6)interrupt_init;定義於cpu/arm920t/s3c24x0/interrupt.c
初始化2410的PWM timer 4,使其能自動裝載計數值,恒定的產生時間中斷信號,但是中斷被遮罩了用不上。
7)env_init;定義於common/env_flash.c(搜索的時候發現別的檔也定義了這個函數,而且沒有巨集定義保證只有一個被編譯,這是個問題,有高手知道指點一下!)
功能:指定環境區的地址。default_environment是默認的環境參數設置。
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = 0;
8)init_baudrate;初始化全域資料區中串列傳輸速率的值
gd->bd->bi_baudrate = gd->baudrate =(i > 0)
? (int) simple_strtoul (tmp, NULL, 10)
: CONFIG_BAUDRATE;
9)serial_init; 串口通訊設置 定義於cpu/arm920t/s3c24x0/serial.c
 根據bd中串列傳輸速率值和pclk,設置串口寄存器。
10)console_init_f;控制台前期初始化common/console.c
由於標準設備還沒有初始化(gd->flags & GD_FLG_DEVINIT=0),這時控制台使用串口作為控制台
函數只有一句:gd->have_console = 1;
10)dram_init,初始化記憶體RAM資訊。board/smdk2410/smdk2410.c
其實就是給gd->bd中記憶體資訊表賦值而已。
gd->bd->bi_dram[0].start = PHYS_SDRAM_1;
gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;
初始化序列表init_sequence[]主要函數分析結束。
11)flash_init;定義在board/smdk2410/flash.c
這個檔與具體平臺關係密切,smdk2410使用的flash與FS2410不一樣,所以移植時這個程式就得重寫。
flash_init()是必須重寫的函數,它做哪些操作呢?
首先是有一個變數flash_info_t flash_info[CFG_MAX_FLASH_BANKS]來記錄flash的資訊。flash_info_t定義:
typedef struct {
ulong size; /* 總大小BYTE */
ushort sector_count; /* 總的sector數*/
ulong flash_id; /* combined device & manufacturer code */
ulong start[CFG_MAX_FLASH_SECT]; /* 每個sector的起始物理位址。 */
uchar protect[CFG_MAX_FLASH_SECT]; /* 每個sector的保護狀態,如果置1,在執行erase操作的時候將跳過對應sector*/
#ifdef CFG_FLASH_CFI //我不管CFI介面。
.....
#endif
} flash_info_t;
flash_init()的操作就是讀取ID號,ID號指明了生產商和設備號,根據這些資訊設置size,sector_count,flash_id.以及start[]、protect[]。
12)把視頻框架緩衝區設置在bss_end後面。
 addr = (_bss_end + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
size = vfd_setmem (addr);
gd->fb_base = addr;
13)mem_malloc_init (_armboot_start - CFG_MALLOC_LEN);
設置heap區,供malloc使用。下麵的變數和函式定義在lib_arm/board.c
malloc可用記憶體由mem_malloc_start,mem_malloc_end指定。而當前分配的位置則是mem_malloc_brk。
mem_malloc_init負責初始化這三個變數。malloc則通過sbrk函數來使用和管理這片記憶體。
static ulong mem_malloc_start = 0;
static ulong mem_malloc_end = 0;
static ulong mem_malloc_brk = 0;

static
void mem_malloc_init (ulong dest_addr)
{
mem_malloc_start = dest_addr;
mem_malloc_end = dest_addr + CFG_MALLOC_LEN;
mem_malloc_brk = mem_malloc_start;

memset ((void *) mem_malloc_start, 0,
mem_malloc_end - mem_malloc_start);
}
void *sbrk (ptrdiff_t increment)
{
ulong old = mem_malloc_brk;
ulong new = old + increment;

if ((new <> mem_malloc_end)) {
return (NULL);
}
mem_malloc_brk = new;
return ((void *) old);
}
14)env_relocate() 環境參數區重定位
由於初始化了heap區,所以可以通過malloc()重新分配一塊環境參數區,
但是沒有必要,因為默認的環境參數已經重定位到RAM中了。
/**這裡發現個問題,ENV_IS_EMBEDDED是否有定義還沒搞清楚,而且CFG_MALLOC_LEN也沒有定義,也就是說如果ENV_IS_EMBEDDED沒有定義則執行malloc,是不是應該有問題?**/
15)IP,MAC地址的初始化。主要是從環境中讀,然後賦給gd->bd對應域就OK。
16)devices_init ();定義於common/devices.c
int devices_init (void)//我去掉了編譯選項,注釋掉的是因為對應的編譯選項沒有定義。
{
devlist = ListCreate (sizeof (device_t));//創建設備清單
i2c_init (CFG_I2C_SPEED, CFG_I2C_SLAVE);//初始化i2c介面,i2c沒有註冊到devlist中去。
//drv_lcd_init ();
//drv_video_init ();
//drv_keyboard_init ();
//drv_logbuff_init ();
drv_system_init ();  //這裡其實是定義了一個串口設備,並且註冊到devlist中。
//serial_devices_init ();
//drv_usbtty_init ();
//drv_nc_init ();
}
  經過devices_init(),創建了devlist,但是只有一個串口設備註冊在內。顯然,devlist中的設備都是可以做為console的。

16) jumptable_init ();初始化gd->jt。1.1.6版本的jumptable只起登記函數位址的作用。並沒有其他作用。
17)console_init_r ();後期控制台初始化
主要過程:查看環境參數stdin,stdout,stderr中對標準IO的指定的設備名稱,再按照環境指定的名稱搜索devlist,將搜到的設備指標賦給標準IO陣列stdio_devices[]。置gd->flag標誌GD_FLG_DEVINIT。這個標誌影響putc,getc函數的實現,未定義此標誌時直接由串口serial_getc和serial_putc實現,定義以後通過標準設備陣列stdio_devices[]中的putc和getc來實現IO。
下面是相關代碼:
void putc (const char c)
{
#ifdef CONFIG_SILENT_CONSOLE
if (gd->flags & GD_FLG_SILENT)//GD_FLG_SILENT無輸出標誌
return;
#endif
if (gd->flags & GD_FLG_DEVINIT) {//設備list已經初始化
/* Send to the standard output */
fputc (stdout, c);
} else {
/* Send directly to the handler */
serial_putc (c);//未初始化時直接從串口輸出。
}
}
void fputc (int file, const char c)
{
if (file <>putc (c);
}

為什麼要使用devlist,std_device[]?

為了更靈活地實現標準IO重定向,任何可以作為標準IO的設備,如USB鍵盤,LCD屏,串口等都可以對應一個device_t的結構體變數,只需要實現getc和putc等函數,就能加入到devlist列表中去,也就可以被assign為標準IO設備std_device中去。如函數

int console_assign (int file, char *devname); /* Assign the console 重定向標準輸入輸出*/

這個函數功能就是把名為devname的設備重定向為標準IO檔file(stdin,stdout,stderr)。其執行過程是在devlist中查找devname的設備,返回這個設備的device_t指標,並把指標值賦給std_device[file]。
18)enable_interrupts(),使能中斷。由於CONFIG_USE_IRQ沒有定義,空實現。
   #ifdef CONFIG_USE_IRQ
/* enable IRQ interrupts */
void enable_interrupts (void)
{
unsigned long temp;
__asm__ __volatile__("mrs %0, cpsr\n"
"bic %0, %0, #0x80\n"
"msr cpsr_c, %0"
: "=r" (temp)
:
: "memory");
}
    #else
void enable_interrupts (void)
{
}
19)設置CS8900的MAC位址。
cs8900_get_enetaddr (gd->bd->bi_enetaddr);
20)初始化乙太網。
eth_initialize(gd->bd);//bd中已經IP,MAC已經初始化
21)main_loop ();定義於common/main.c
至此所有初始化工作已經完畢。main_loop在標準轉入設備中接受命令列,然後分析,查找,執行。

關於U-boot中命令相關的程式設計:

1、命令相關的函數和定義
@main_loop:這個函數裡有太多編譯選項,對於smdk2410,去掉所有選項後等效下面的程式
void main_loop()
{
static char lastcommand[CFG_CBSIZE] = { 0, };
int len;
int rc = 1;
int flag;
char *s;
int bootdelay;
s = getenv ("bootdelay"); //自動啟動內核等待延時
bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;

debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay);
s = getenv ("bootcmd"); //取得環境中設置的啟動命令列
debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "");

if (bootdelay >= 0 && s && !abortboot (bootdelay))
{
run_command (s, 0);//執行啟動命令列,smdk2410.h中沒有定義CONFIG_BOOTCOMMAND,所以沒有命令執行。
}

for (;;) {
len = readline(CFG_PROMPT);//讀取鍵入的命令列到console_buffer

flag = 0; /* assume no special flags for now */
if (len > 0)
strcpy (lastcommand, console_buffer);//拷貝命令列到lastcommand.
else if (len == 0)
flag |= CMD_FLAG_REPEAT;
if (len == -1)
puts ("\n");
else
rc = run_command (lastcommand, flag); //執行這個命令列。
if (rc <= 0) { /* invalid command or not repeatable, forget it */ lastcommand[0] = 0; } }


@run_comman();在命令table中查找匹配的命令名稱,得到對應命令結構體變數指標,以解析得到的參數調用其處理函數執行命令。 @命令結構構體類型定義:command.h中,struct cmd_tbl_s


{ char *name; /* 命令名 */


int maxargs; /* 最大參數個數maximum number of arguments */


int repeatable; /* autorepeat allowed? */


/* Implementation function 命令執行函數*/


int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);


char *usage; /* Usage message (short) */


#ifdef CFG_LONGHELP


char *help; /* Help message (long) */


#endif




#ifdef CONFIG_AUTO_COMPLETE


/* do auto completion on the arguments */


int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);


#endif };


typedef struct cmd_tbl_s cmd_tbl_t; //定義section屬性的結構體。編譯的時候會單獨生成一個名為.u_boot_cmd的section段。


#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd"))) //這個巨集定義一個命令結構體變數。並用name,maxargs,rep,cmd,usage,help初始化各個域。


#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \


cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}


2、在u-boot中,如何添加一個命令:


1)CFG_CMD_* 命令選項位元標誌。在include/cmd_confdefs.h 中定義。 每個板子的設定檔(如include/config/smdk2410.h)中都可以定義u-boot 需要的命令,如果要添加一個命令,必須添加相應的命令選項。如下:


#define CONFIG_COMMANDS \


(CONFIG_CMD_DFL | \


CFG_CMD_CACHE | \


/*CFG_CMD_NAND |*/ \


/*CFG_CMD_EEPROM |*/ \


/*CFG_CMD_I2C |*/ \


/*CFG_CMD_USB |*/ \


CFG_CMD_REGINFO | \


CFG_CMD_DATE | \


CFG_CMD_ELF)


定義這個選項主要是為了編譯命令需要的原始檔案,大部分命令都在common資料夾下對應一個原始檔案 cmd_*.c,如cmd_cache.c實現cache命令。


檔開頭就有一行編譯條件:


#if(CONFIG_COMMANDS&CFG_CMD_CACHE) 也就是說,如果配置標頭檔中CONFIG_COMMANDS不或上相應命令的選項,這裡就不會被編譯。


2)定義命令結構體變數,如:


U_BOOT_CMD( dcache, 2, 1, do_dcache, "dcache - enable or disable data cache\n", "[on, off]\n" " - enable or disable data (writethrough) cache\n" );


其實就是定義了一個cmd_tbl_t類型的結構體變數,這個結構體變數名為__u_boot_cmd_dcache。 其中變數的五個域初始化為括弧的內容。分別指明了命令名,參數個數,重複數,執行命令的函數,命令提示。 每個命令都對應這樣一個變數,同時這個結構體變數的section屬性為.u_boot_cmd.也就是說每個變數編譯結束 在目的檔案中都會有一個.u_boot_cmd的section.一個section是連接時的一個輸入段,如.text,.bss,.data等都是section名。 最後由連結程式把所有的.u_boot_cmd段連接在一起,這樣就組成了一個命令結構體陣列。 u-boot.lds中相應腳本如下: . = .; __u_boot_cmd_start = .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end = .; 可以看到所有的命令結構體變數集中在__u_boot_cmd_start開始到__u_boot_cmd_end結束的連續位址範圍內, 這樣形成一個cmd_tbl_t類型的陣列,run_command函數就是在這個陣列中查找命令的。


3)實現命令處理函數。命令處理函數的格式: void function (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]) 總體來說,如果要實現自己的命令,應該在include/com_confdefs.h中定義一個命令選項標誌位元。 在板子的設定檔中添加命令自己的選項。按照u-boot的風格,可以在common/下面添加自己的cmd_*.c,並且定義自己的命令結構體變數,如U_BOOT_CMD( mycommand, 2, 1, do_mycommand, "my command!\n", "...\n" " ..\n" ); 然後實現自己的命令處理函數do_mycommand(cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])。


四、U-boot在ST2410的移植,基於NOR FLASH和NAND FLASH啟動。


1、從smdk2410到ST2410: ST2410板子的核心板與FS2410是一樣的。我沒有整到smdk2410的原理圖,從網上得知的結論總結如下, fs2410與smdk2410 RAM位址空間大小一致(0x30000000~0x34000000=64MB); NOR FLASH型號不一樣,FS2410用SST39VF1601系列的,smdk2410用AMD產LV系列的; 網路晶片型號和在記憶體中映射的位址完全一致(CS8900,IO方式基底位址0x19000300)


2、移植過程: 移植u-boot的基本步驟如下


(1) 在頂層Makefile中為開發板添加新的配置選項,使用已有的配置專案為例。 smdk2410_config : unconfig @./mkconfig $(@:_config=) arm arm920t smdk2410 NULL s3c24×0 參考上面2行,添加下面2行。 fs2410_config : unconfig @./mkconfig $(@:_config=) arm arm920t fs2410 NULL s3c24×0


(2) 創建一個新目錄存放開發板相關的代碼,並且添加檔。 board/fs2410/config.mk board/fs2410/flash.c board/fs2410/fs2410.c board/fs2410/Makefile board/fs2410/memsetup.S board/fs2410/u-boot.lds 注意將board/fs2410/Makefile中smdk2410.o全部改為fs2410.o


(3) 為開發板添加新的設定檔 可以先複製參考開發板的設定檔,再修改。例如: $cp include/configs/smdk2410.h include/configs/fs2410.h 如果是為一顆新的CPU移植,還要創建一個新的目錄存放CPU相關的代碼。


(4) 配置開發板 $ make fs2410_config


3、移植要考慮的問題:


從smdk2410到ST2410移植要考慮的主要問題就是NOR flash。從上述分析知道,u-boot啟動時要執行flash_init() 檢測flash的ID號,大小,secotor起始位址表和保護狀態表,這些資訊全部保存在flash_info_t flash_info[CFG_MAX_FLASH_BANKS]中。


另外,u-boot中有一些命令如saveenvt需要要擦寫flash,間接調用兩個函數:flash_erase和write_buff。在board/smdk2410/flash.c 實現了與smdk2410板子相關的nor flash函數操作。由於write_buffer中調用了write_hword去具體寫入一個字到flash中,這個函數本身是與硬體無關的, 所以與硬體密切相關的三個需要重寫的函數是flash_init, flash_erase,write_hword;


4、SST39VF1601: FS2410板nor flash型號是SST39VF1601,根據data sheet,其主要特性如下: 16bit字為訪問單位。2MBTYE大小。 sector大小2kword=4KB,block大小32Kword=64KB;這裡我按block為單位管理flash,即flash_info結構體變數中的sector_count是block數,起始位址表保存也是所有block的起始位址。 SST Manufacturer ID = 00BFH ; SST39VF1601 Device ID = 234BH; 軟體命令序列如最上面圖。


5、我實現的flash.c主要部分: //相關定義:


# define CFG_FLASH_WORD_SIZE unsigned short //訪問單位為16b字


#define MEM_FLASH_ADDR1 (*(volatile CFG_FLASH_WORD_SIZE *)(CFG_FLASH_BASE + 0x000005555<<1>


#define MEM_FLASH_ADDR2 (*(volatile CFG_FLASH_WORD_SIZE *)(CFG_FLASH_BASE + 0x000002AAA<<1 i =" 0;" flash_id="FLASH_UNKNOWN;" info="(flash_info_t" mem_flash_addr1="(CFG_FLASH_WORD_SIZE)(0x00AA);" mem_flash_addr2="(CFG_FLASH_WORD_SIZE)(0x0055);" mem_flash_addr1="(CFG_FLASH_WORD_SIZE)(0x0090);" value="READ_ADDR0;" value="=">flash_id = FLASH_MAN_SST;
else
{
panic("NOT expected FLASH FOUND!\n");return 0;
}
value=READ_ADDR1; //read device ID

if(value==(CFG_FLASH_WORD_SIZE)SST_ID_xF1601)
{
info->flash_id += FLASH_SST1601;
info->sector_count = 32; //32 block
info->size = 0x00200000; // 2M=32*64K
}
else
{
panic("NOT expected FLASH FOUND!\n");return 0;
}

//建立sector起始地址表。
if ((info->flash_id & FLASH_VENDMASK) == FLASH_MAN_SST )
{
for (i = 0; i <>sector_count; i++)
info->start[i] = CFG_FLASH_BASE + (i * 0x00010000);
}

//設置sector保護資訊,對於SST生產的FLASH,全部設為0。
for (i = 0; i <>sector_count; i++)
{
if((info->flash_id & FLASH_VENDMASK) == FLASH_MAN_SST)
info->protect[i] = 0;
}

//結束讀ID狀態:
*((CFG_FLASH_WORD_SIZE *)&info->start[0])= (CFG_FLASH_WORD_SIZE)0x00F0;

//設置保護,將u-boot鏡像和環境參數所在的block的proctect標誌置1
flash_protect (FLAG_PROTECT_SET,
CFG_FLASH_BASE,
CFG_FLASH_BASE + monitor_flash_len - 1,
&flash_info[0]);

flash_protect (FLAG_PROTECT_SET,
CFG_ENV_ADDR,
CFG_ENV_ADDR + CFG_ENV_SIZE - 1, &flash_info[0]);
return info->size;
}
   
//flash_erase實現
 這裡給出修改的部分,s_first,s_last是要擦除的block的起始和終止block號.對於protect[]置位的block不進行擦除。
擦除一個block命令時序按照上面圖示的Block-Erase進行。
for (sect = s_first; sect<=s_last; sect++) { if (info->protect[sect] == 0)
{ /* not protected */
addr = (CFG_FLASH_WORD_SIZE *)(info->start[sect]);
if ((info->flash_id & FLASH_VENDMASK) == FLASH_MAN_SST)
{
MEM_FLASH_ADDR1 = (CFG_FLASH_WORD_SIZE)0x00AA;
MEM_FLASH_ADDR2 = (CFG_FLASH_WORD_SIZE)0x0055;
MEM_FLASH_ADDR1 = (CFG_FLASH_WORD_SIZE)0x0080;
MEM_FLASH_ADDR1 = (CFG_FLASH_WORD_SIZE)0x00AA;
MEM_FLASH_ADDR2 = (CFG_FLASH_WORD_SIZE)0x0055;
addr[0] = (CFG_FLASH_WORD_SIZE)0x0050; /* block erase */
for (i=0; i<50; start =" get_timer" last =" start;" addr =" (CFG_FLASH_WORD_SIZE">start[l_sect]);//查詢DQ7是否為1,DQ7=1表明擦除完畢
while ((addr[0] & (CFG_FLASH_WORD_SIZE)0x0080) != (CFG_FLASH_WORD_SIZE)0x0080) {
if ((now = get_timer(start)) > CFG_FLASH_ERASE_TOUT) {
printf ("Timeout\n");
return 1;
}
................

//write_word操作,這個函數由write_buff一調用,完成寫入一個word的操作,其操作命令序列由上圖中Word-Program指定。
static int write_word (flash_info_t *info, ulong dest, ulong data)
{
volatile CFG_FLASH_WORD_SIZE *dest2 = (CFG_FLASH_WORD_SIZE *)dest;
volatile CFG_FLASH_WORD_SIZE *data2 = (CFG_FLASH_WORD_SIZE *)&data;
ulong start;
int flag;
int i;

/* Check if Flash is (sufficiently) erased */
if ((*((volatile ulong *)dest) & data) != data) {
return (2);
}
/* Disable interrupts which might cause a timeout here */
flag = disable_interrupts();

for (i=0; i<4/sizeof(cfg_flash_word_size); mem_flash_addr1 =" (CFG_FLASH_WORD_SIZE)0x00AA;" mem_flash_addr2 =" (CFG_FLASH_WORD_SIZE)0x0055;" mem_flash_addr1 =" (CFG_FLASH_WORD_SIZE)0x00A0;" start =" get_timer"> CFG_FLASH_WRITE_TOUT) {
return (1);
}
}
}
return (0);
}

這些代碼在與nor flash相關的命令中都會間接被調用。所以u-boot可攜性的另一個方面就是規定一些函式呼叫介面和全域變數,這些函數的實現是硬體相關的,移植時只需要實現這些函數。
而全域變數是具體硬體無關的。u-boot在通用目錄中實現其餘與硬體無關的函數,這些函數就只與全域變數和函數介面打交道了。 通過編譯選項設置來靈活控制是否需要編譯通用部分。
6、增加從Nand 啟動的代碼:
FS2410板有跳線,跳線短路時從NAND啟動,否則從NOR啟動。根據FS2410 BIOS源碼,我修改了start.s加入了可以從兩種FLASH中啟動u-boot的
代碼。原理在於:在重定位之前先讀BWSCON寄存器,判斷OM0位是0(有跳線,NAND啟動)還是1(無跳線,NOR啟動),採取不同的重定位代碼
分別從nand或nor中拷貝u-boot鏡像到RAM中。這裡面也有問題,比如從Nand啟動後,nor flash的初始化代碼和與它相關的命令都是不能使用的。
這裡我採用比較簡單的方法,定義一個全域變數標誌_boot_flash保存當前啟動FLASH標誌,_boot_flash=0則表明是NOR啟動,否則是從NAND。
在每個與nor flash 相關的命令執行函數一開始就判斷這個變數,如果為1立即返回。flash_init()也必須放在這個if(!_boot_flash)條件中。
這裡方法比較笨,主要是為了能在跳線處於任意狀態時都能啟動u-boot。
修改後的start.s如下。
.......
//修改1
.globl _boot_flash
_boot_flash: //定義全域標誌變數,0:NOR FLASH啟動,1:NAND FLASH啟動。
.word 0x00000000
.........

///修改2:

ldr r0,=BWSCON
ldr r0,[r0]
ands r0,r0,#6
beq nand_boot //OM0=0,有跳線,從Nand啟動。nand_boot在後面定義。
............
//修改4,這裡在全域變數_boot_flash中設置當前啟動flash設備是NOR還是NAND
//這裡已經完成搬運到RAM的工作,即將跳轉到RAM中_start_armboot函數中執行。
adr r1,_boot_flash //取_boot_flash的當前位址,這時還在NOR FLASH或者NAND 4KB緩衝中。
ldr r2,_TEXT_BASE
add r1,r1,r2 //得到_boot_flash重定位後的地址,這個地址在RAM中。
ldr r0,=BWSCON
ldr r0,[r0]
ands r0,r0,#6 //
mov r2,#0x00000001
streq r2,[r1] //如果當前是從NAND啟動,置_boot_flash為1

ldr pc, _start_armboot

_start_armboot: .word start_armboot

........

//////// 修改4,從NAND拷貝U-boot鏡像(最大128KB),這段代碼由fs2410 BIOS修改得來。
nand_boot:
mov r5, #NFCONF
ldr r0, =(1<<15)|(1<<12)|(1<<11)|(7<<8)|(7<<4)|(7) block="32" block="32" 512="128KB。" id =" RdNFDat()<<8;" r1 =" RdNFDat();">>8)
strb r1,[r5,#8]
cmp r6,#0 //if(NandAddr)
movne r0,r0,lsr #16 //WrNFAddr(addr>>16)
strneb r0,[r5,#8]

bl WaitNandBusy //WaitNFBusy()

ldrb r0, [r5,#0xc] //RdNFDat()
sub r0, r0, #0xff

mov r1,#0 //WrNFCmd(READCMD0)
strb r1,[r5,#4]

ldr r1,[r5,#0] //NFChipDs()
orr r1,r1,#0x800
str r1,[r5,#0]

mov pc, r7

ReadNandPage:
mov r7,lr
mov r4,r1
mov r5,#NFCONF

ldr r1,[r5,#0] //NFChipEn()
bic r1,r1,#0x800
str r1,[r5,#0]

mov r1,#0 //WrNFCmd(READCMD0)
strb r1,[r5,#4]
strb r1,[r5,#8] //WrNFAddr(0)
strb r0,[r5,#8] //WrNFAddr(addr)
mov r1,r0,lsr #8 //WrNFAddr(addr>>8)
strb r1,[r5,#8]
cmp r6,#0 //if(NandAddr)
movne r0,r0,lsr #16 //WrNFAddr(addr>>16)
strneb r0,[r5,#8]

ldr r0,[r5,#0] //InitEcc()
orr r0,r0,#0x1000
str r0,[r5,#0]

bl WaitNandBusy //WaitNFBusy()

mov r0,#0 //for(i=0; i<512;>

2010年2月2日 星期二

mini2440學習重點描述


建議採用超級終端機,因為輸入時secure-CRT無法讓我輸入,可能是我哪裡有設定錯誤。
由於現在用usb轉serial的users很多,所以就擴充更多com port.

下圖是簡體版,顯示亂碼,上圖繁體版本

以下網址是學習embeded linux的相關網址:
1)當mini2440使用nor flash啟動時,因為它會直接往com port送出訊號,此時開發板必須在先前便接好連線,並在工作主機啟動任一終端機軟體,並設好鮑率(Buat Rate),等待接收訊號。便會得到如上圖所示畫面。這是因為mini2440買來時NOR/NAND Flash都已經燒錄supervivi在裡頭。只是它們以不同的方式被執行,所見到的UI介面也不同。NAND Flash的啟動甚至讓你感覺不到bootloader的存在,因為當它初使化完畢接著就馬上去載入linux系統;所以你只能看到linux開機畫面。
2)將dnw.exe(繁體)中文化:實際狀況可參考上圖。

2010年2月1日 星期一

u-boot的Makefile分析

u-boot的Makefile分析

個人覺得要搞懂u-boot的確是個大工程,但我非常確定這個起點非常重要,所以為了馬步要蹲好,除了makefile以外,各個重要的標頭檔一定要先著磨,並記下筆記。文章出處:http://blog.mcuol.com/User/lvembededsys/Article/4355_1.htm
U-BOOT是一個LINUX下的工程,在編譯之前必須已經安裝對應體系結構的交叉編譯環境,這裡只針對ARM,編譯器系列軟體為arm-linux-*。
U-BOOT的下載地址: http://sourceforge.net/projects/u-boot我下載的是1.1.6版本,一開始在FTP上下載了一個次新版,結果編譯失敗。1.1.6是沒問題的。
u-boot源碼結構 解壓就可以得到全部u-boot來源程式。在頂層目錄下有18個子目錄,分別存放和管理不同的來源程式。這些目錄中所要存放的檔有其規則,可以分為3類。 第1類目錄與處理器體系結構或者開發板硬體直接相關; 第2類目錄是一些通用的函數或者驅動程式; 第3類目錄是u-boot的應用程式、工具或者文檔。u-boot的源碼頂層目錄說明
目 錄 特 性 解 釋 說 明
board 平臺依賴 存放電路板相關的目錄檔
例如:RPXlite(mpc8xx)、smdk2410(arm920t)、sc520_cdp(x86) 等目錄

cpu 平臺依賴 存放CPU相關的目錄檔
例如:mpc8xx、ppc4xx、arm720t、arm920t、 xscale、i386等目錄
lib_ppc 平臺依賴 存放對PowerPC體系結構通用的檔
主要用於實現PowerPC平臺通用的函數
lib_arm 平臺依賴 存放對ARM體系結構通用的檔
主要用於實現ARM平臺通用的函數
lib_i386 平臺依賴 存放對X86體系結構通用的檔
主要用於實現X86平臺通用的函數
include 通用 標頭檔和開發板設定檔
所有開發板的設定檔都在configs目錄下
common 通用 通用的多功能函數實現
lib_generic 通用 通用庫函數的實現
net 通用 存放網路的程式
fs 通用 存放檔案系統的程式
post 通用 存放上電自檢程式
drivers 通用 通用的設備驅動程式,主要有乙太網介面的驅動
disk 通用 硬碟介面程式
rtc  通用 RTC的驅動程式
dtt  通用 數位溫度測量器或者感測器的驅動
examples 應用常式 一些獨立運行的應用程式的例子,例如helloworld
tools 工具 存放製作S-Record或者u-boot格式的映射等工具
例如mkimage
doc 文檔 開發使用文檔
u-boot的原始程式碼包含對幾十種處理器、數百種開發板的支援。可是對於特定的開發板,配置編譯過程只需要其中部分程式。這裡具體以S3C2410 & arm920t處理器為例,具體分析S3C2410處理器和開發板所依賴的程式,以及u-boot的通用函數和工具。
編譯以smdk_2410板為例,編譯的過程分兩部:
# make smdk2410_config# make
頂層Makefile分析
要瞭解一個LINUX工程的結構必須看懂Makefile,尤其是頂層的,沒辦法,UNIX世界就是這麼無奈,什麼東西都用文檔去管理、配置。首先在這方面我是個新手,時間所限只粗淺地看了一些Makefile規則。
以smdk_2410為例,順序分析Makefile大致的流程及結構如下:
1) Makefile中定義了源碼及生成的目的檔案存放的目錄,目的檔案存放目錄BUILD_DIR可以通過make O=dir 指定。如果沒有指定,則設定為源碼頂層目錄。一般編譯的時候不指定輸出目錄,則BUILD_DIR為空。其它目錄變數定義如下:
#OBJTREE和LNDIR為存放生成檔的目錄,TOPDIR與SRCTREE為源碼所在目錄
OBJTREE := $(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR))
SRCTREE := $(CURDIR)
TOPDIR := $(SRCTREE)
LNDIR := $(OBJTREE)
export TOPDIR SRCTREE OBJTREE
2)定義變數MKCONFIG:這個變數指向一個腳本,即頂層目錄的mkconfig。
MKCONFIG := $(SRCTREE)/mkconfigexport MKCONFIG
在編譯U-BOOT之前,先要執行
# make smdk2410_config
smdk2410_config是Makefile的一個目標,定義如下:
smdk2410_config : unconfig
@$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 NULL s3c24x0
unconfig::
@rm -f $(obj)include/config.h $(obj)include/config.mk \ $(obj)board/*/config.tmp $(obj)board/*/*/config.tmp
顯然,執行# make smdk2410_config時,先執行unconfig目標,注意不指定輸出目標時,obj,src變數均為空,unconfig下面的命令清理上一次執行make *_config時生成的標頭檔和makefile的包含檔。主要是include/config.h 和include/config.mk文件。
然後才執行命令
@$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 NULL s3c24x0
MKCONFIG 是頂層目錄下的mkcofig指令檔,後面五個是傳入的參數。
對於smdk2410_config而言,mkconfig主要做三件事:
在include資料夾下建立相應的文件(夾)軟連接,
#如果是ARM體系將執行以下操作:
#ln -s asm-arm asm
#ln -s arch-s3c24x0 asm-arm/arch
#ln -s proc-armv asm-arm/proc
生成Makefile包含檔include/config.mk,內容很簡單,定義了四個變數:
ARCH = arm
CPU = arm920t
BOARD = smdk2410
SOC = s3c24x0
生成include/config.h標頭檔,只有一行:
/* Automatically generated - do not edit */#include "config/smdk2410.h"
mkconfig指令檔的執行至此結束,繼續分析Makefile剩下部分。
3)包含include/config.mk,其實也就相當於在Makefile裡定義了上面四個變數而已。
4) 指定交叉編譯器首碼:
ifeq ($(ARCH),arm)#這裡根據ARCH變數,指定編譯器首碼。
CROSS_COMPILE = arm-linux-endif
5)包含config.mk:
#包含頂層目錄下的config.mk,這個檔裡面主要定義了交叉編譯器及選項和編譯規則
# load other configurationinclude $(TOPDIR)/config.mk
下面分析config.mk的內容:
@包含體系,開發板,CPU特定的規則檔:
ifdef ARCH #指定預編譯體系結構選項
sinclude $(TOPDIR)/$(ARCH)_config.mk
# include architecture dependend rules
endif

ifdef CPU #定義編譯時對齊,浮點等選項
sinclude $(TOPDIR)/cpu/$(CPU)/config.mk
# include CPU specific rules
endif


ifdef SOC #沒有這個檔
sinclude $(TOPDIR)/cpu/$(CPU)/$(SOC)/config.mk
# include SoC specific rules
endif


ifdef BOARD #指定特定板子的鏡像連接時的記憶體基底位址,重要!
sinclude $(TOPDIR)/board/$(BOARDDIR)/config.mk
# include board specific rules
endif

@定義交叉編譯鏈工具
# Include the make variables (CC, etc...)
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
RANLIB = $(CROSS_COMPILE)RANLIB

@定義AR選項ARFLAGS,調試選項DBGFLAGS,優化選項OPTFLAGS
 預處理選項CPPFLAGS,C編譯器選項CFLAGS,連接選項LDFLAGS
LDFLAGS += -Bstatic -T $(LDSCRIPT) -Ttext $(TEXT_BASE) $(PLATFORM_LDFLAGS) #指定了起始位址TEXT_BASE
@指定編譯規則:
$(obj)%.s: %.S
$(CPP) $(AFLAGS) -o $@ $< $(obj)%.o: %.S $(CC) $(AFLAGS) -c -o $@ $< $(obj)%.o: %.c $(CC) $(CFLAGS) -c -o $@ $< 回到頂層makefile文件: 6)U-boot需要的目的檔案。
OBJS = cpu/$(CPU)/start.o # 順序很重要,start.o必須放第一位
7)需要的庫檔:
LIBS = lib_generic/libgeneric.a
LIBS += board/$(BOARDDIR)/lib$(BOARD).a
LIBS += cpu/$(CPU)/lib$(CPU).a
ifdef SOC
LIBS += cpu/$(CPU)/$(SOC)/lib$(SOC).a
endif
LIBS += lib_$(ARCH)/lib$(ARCH).a
LIBS += fs/cramfs/libcramfs.a fs/fat/libfat.a fs/fdos/libfdos.a fs/jffs2/libjffs2.a \ fs/reiserfs/libreiserfs.a fs/ext2/libext2fs.a #延續上一行
LIBS += net/libnet.a
LIBS += disk/libdisk.a
LIBS += rtc/librtc.a
LIBS += dtt/libdtt.a
LIBS += drivers/libdrivers.a
LIBS += drivers/nand/libnand.a
LIBS += drivers/nand_legacy/libnand_legacy.a
LIBS += drivers/sk98lin/libsk98lin.a
LIBS += post/libpost.a post/cpu/libcpu.a
LIBS += common/libcommon.a
LIBS += $(BOARDLIBS)
LIBS := $(addprefix $(obj),$(LIBS))
.PHONY : $(LIBS)
根據上面的include/config.mk檔定義的ARCH、CPU、BOARD、SOC這些變數。硬體平臺依賴的目錄檔可以根據這些定義來確定。
SMDK2410平臺相關目錄及對應生成的庫檔如下。
board/smdk2410/ :庫文件board/smdk2410/libsmdk2410.a
cpu/arm920t/ :庫文件cpu/arm920t/libarm920t.a
cpu/arm920t/s3c24x0/ : 庫文件cpu/arm920t/s3c24x0/libs3c24x0.a
lib_arm/ : 庫文件lib_arm/libarm.a
include/asm-arm/ :下面兩個是標頭檔。
include/configs/smdk2410.h
8)最終生成的各種鏡像檔:
ALL = $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND)
all: $(ALL)
$(obj)u-boot.hex: $(obj)u-boot
$(OBJCOPY) ${OBJCFLAGS} -O ihex $< $@ $(obj)u-boot.srec: $(obj)u-boot $(OBJCOPY) ${OBJCFLAGS} -O srec $< $@ $(obj)u-boot.bin: $(obj)u-boot $(OBJCOPY) ${OBJCFLAGS} -O binary $< $@ #這裡生成的是U-boot的ELF檔鏡像 $(obj)u-boot: depend version $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT) UNDEF_SYM=`$(OBJDUMP) -x $(LIBS) sed -n -e ''''''''''''''''''''''''''''''''s/.*\(__u_boot_cmd_.*\)/-u\1/p''''''''''''''''''''''''''''''''sortuniq`;\ cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \ --start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \ -Map u-boot.map -o u-boot 分析一下最關鍵的u-boot ELF檔鏡像的生成: @依賴目標depend :生成各個子目錄的.depend文件,.depend列出每個目的檔案的依賴檔。生成方法,調用每個子目錄的make _depend。 depend dep: for dir in $(SUBDIRS) ; do $(MAKE) -C $$dir _depend ; done @依賴目標version:生成版本資訊到版本檔VERSION_FILE中。
version:
@echo -n "#define U_BOOT_VERSION \"U-Boot " > $(VERSION_FILE); \
echo -n "$(U_BOOT_VERSION)" >> $(VERSION_FILE); \
echo -n $(shell $(CONFIG_SHELL) $(TOPDIR)/tools/setlocalversion \
$(TOPDIR)) >> $(VERSION_FILE); \
echo "\"" >> $(VERSION_FILE)
@偽目標SUBDIRS: 執行tools ,examples ,post,post\cpu 子目錄下麵的make檔。
SUBDIRS = tools \
examples \
post \
post/cpu
.PHONY : $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@ all
@依賴目標$(OBJS),即cpu/start.o
$(OBJS): $(MAKE) -C cpu/$(CPU) $(if $(REMOTE_BUILD),$@,$(notdir $@))
@依賴目標$(LIBS),這個目標太多,都是每個子目錄的庫檔*.a ,通過執行相應子目錄下的make來完成:
$(LIBS): $(MAKE) -C $(dir $(subst $(obj),,$@))
@依賴目標$(LDSCRIPT)
LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds
LDFLAGS += -Bstatic -T $(LDSCRIPT) -Ttext $(TEXT_BASE) $(PLATFORM_LDFLAGS)
對於smdk2410,LDSCRIPT即連接指令檔是board/smdk2410/u-boot.lds,定義了連接時各個目的檔案是如何組織的。內容如下:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS{
. = 0x00000000;
. = ALIGN(4);

.text :
/*.text的基底位址由LDFLAGS中-Ttext $(TEXT_BASE)指定*/
{

/*smdk2410指定的基底位址為0x33f80000*/
cpu/arm920t/start.o (.text) /*start.o為首*/

*(.text) }
. = ALIGN(4); .rodata : { *(.rodata) }
. = ALIGN(4); .data : { *(.data) }
. = ALIGN(4); .got : { *(.got) }
. = .; __u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) }
_end = .;
}
@執行連接命令:
cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \
--start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \
-Map u-boot.map -o u-boot
其實就是把start.o和各個子目錄makefile生成的庫檔按照LDFLAGS連接在一起,生成ELF檔u-boot 和連接時記憶體分配圖檔u-boot.map。
9)對於各子目錄的makefile檔,主要是生成*.o檔然後執行AR生成對應的庫檔。如lib_generic資料夾Makefile:
LIB = $(obj)libgeneric.a
COBJS = bzlib.o bzlib_crctable.o bzlib_decompress.o \
bzlib_randtable.o bzlib_huffman.o \
crc32.o ctype.o display_options.o ldiv.o \
string.o vsprintf.o zlib.o
SRCS := $(COBJS:.o=.c)
OBJS := $(addprefix $(obj),$(COBJS))
$(LIB): $(obj).depend $(OBJS) #項層Makefile執行make libgeneric.a
$(AR) $(ARFLAGS) $@ $(OBJS)
整個makefile剩下的內容全部是各種不同的開發板的*_config:目標的定義了。
概括起來,工程的編譯流程也就是通過執行執行一個make *_config傳入ARCH,CPU,BOARD,SOC參數,mkconfig根據參數將include標頭檔夾相應的標頭檔夾連接好,生成config.h。然後執行make分別調用各子目錄的makefile 生成所有的obj檔和obj庫檔*.a. 最後連接所有目的檔案,生成鏡像。不同格式的鏡像都是調用相應工具由elf鏡像直接或者間接生成的。
剩下的工作就是分析U-Boot原始程式碼了

2010年1月26日 星期二

MemTest86 + 4.00 pattern說明

注意;以下的陣列可能因版本不同會有修改;此處版本是memtest86 + 4.00
const struct tseq tseq[] = {
{1, 5, 4, 0, "[Address test, walking ones] "}, //pattern0
{1, 6, 4, 0, "[Address test, own address] "}, //pattern1
{1, 0, 4, 0, "[Moving inversions, ones & zeros] "}, //pattern2
{1, 1, 2, 0, "[Moving inversions, 8 bit pattern] "}, //pattern3
{1, 10, 50, 0, "[Moving inversions, random pattern] "}, //pattern4
{1, 7, 80, 0, "[Block move, 80 moves] "}, //pattern5
{1, 2, 2, 0, "[Moving inversions, 32 bit pattern] "}, //pattern6
{1, 9, 30, 0, "[Random number sequence] "}, //pattern7
{1, 11, 6, 0, "[Modulo 20, Random pattern] "}, //pattern8
{1, 8, 1, 0, "[Bit fade test, 90 min, 2 patterns] "}, //pattern9
{0, 0, 0, 0, NULL}
};
正常狀況下開機後只會測試pattern1~pattern8,因此若要測試pattern0及pattern9只好手動設定。
來看一下tsaq結構:以下的結構就是對應到上面的陣列元素
struct tseq {
short cache; //1:打開cpu快取;o:關閉快取
short pat; //pattern id注意這和patter執行順序無關
short iter; //pattern測試的重複次數
short errors; //此攔位一看就知道記錄著個別pattern error數量
char *msg;//這就是pattern的名稱:例如 "Address test, walking ones"
};

2010年1月19日 星期二

AT&T 組合語言指令詳解2

這段時間在研究Linux源代碼,遇到了AT&T彙編,故轉貼個不錯的AT&T彙編貼如下

就像軟體的真諦——“給我一個標準,我用我的邏輯舞動世界”一樣,AT&T 彙編是組合語言裏的另一種標準,這是相對於鼎鼎大名的intel的x86彙編來說。即使對於電子專業的學生來說,一旦掌握了c51單片機的彙編,x86的彙編也已經入門了,但x86畢竟有著強大的寄存器,在串操作指令和作業系統類指令方面,單片機還是望塵莫及的。

下面的兩段話就輕易的涉及到了關於首碼、運算元的方向、操作碼的尾碼這些概念的不同。關於首碼,AT&T 彙編中,寄存器前被冠以“%”,立即數前被冠以“$”,十六進位數前被冠以“0x”。所以,如果gemfield在AT&T 語境中說到386的通用寄存器時,會這樣描述:8個32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp。比如:在stage1.s中,有這麼一個定義,#define ABS(x) (x-_start+0x7c00),那麼你就會知到0x7c00是個十六進位數(_start函數的入口位址就位於記憶體的0x7c00處)。

而在設置int 0x13的0x42功能號時,它是這麼說的:movb $0x42,%ah 。這句告訴了我們一些不同之處:首先,操作碼的尾碼l表示的是操作碼的大小,l是長整數32位元,那麼相應的,movw是16位,movb是8位;其次,立即數是用$首碼來表示的,就像$0x42;再次,寄存器的名字是有%首碼的,像例子中的%ah;最後,運算元的方向有點不一樣,比如把立即數$0x42放到寄存器%ah中,用的是movb $0x42,%ah,也即源運算元在前,目的運算元在後。

對於記憶體單元運算元來說,在AT&T 中是把寄存器用()括起來,而非【】。比如,movl %ebx,8(%si),將ebx寄存器裏的值放到記憶體位址是8(%si)的記憶體單元上。正好,這裏同時遇到了另一個問題,就是在AT&T 彙編中,間接定址方式是有別於x86的。上例中的8(%si)就相當於x86中的【si+8】。

還有一種叫做label(標號)的程式控制語句,比如,在stage1.s中,有這麼一段指令:

cmpb $GRUB_INVALID_DRIVE,%al

je 1f

movb %al,%dl

1:

pushw %dx

上面就用到了標號,je1f,前面的兩個數進行比較,如果相等就跳轉到1的位置。注意,1後面的f表示的是forward,即從je指令後繼續往前走來尋找1這個標號。所以,如果程式中有好幾個叫做1的標號,就要看是1f還是1b了,b代表backward,方向和f相反。青島之光論壇裏有這麼一個例子可以更好的幫助我們理解:

je 1f或者je 1b 是跳轉到對應的標號的地方。這裏的1表示標號(label),f和b表示向前還是向後,f(forward)向前,b(backward)向後,如下例:
1行 1: cmp $0, (%si)
2行 je 1f ///////跳轉到後面的1標示的地方,也就是第6行
3行 movsb
4行 stosb
5行 jmp 1b ////////跳轉到前面1表示的地方 ,也就是第1行
6行 1: jmp 1b ////////跳轉到前面1表示的地方,第6行,其實就是個閉環

然後,在AT&T 彙編中出現最多的大概就是稱作assembler directive的東西了,我們稱作“AT&T 組合語言程式代碼控制”。所有的彙編器命令名都由句號('.')開頭。命令名的其餘是字母,通常使用小寫。在青島之光論壇 http://www.civilnet.cn/bbs/read.php?tid=970 處有一篇關於程式碼控制的詳細介紹,下面gemfield只挑出幾個常用的來說明一下。

一、.byte 運算式(expression_rs)

.byte.byte可不帶參數或者帶多個運算式參數,運算式之間由逗點分隔。每個運算式參數都被彙編成下一個位元組。在stage1.s中,有這麼一段代碼:

after_BPB:

CLI

.byte 0x80,0xca

那麼編譯器在編譯時,就會在cli指令的下面接著放上0x80和0xca,因為每個運算式要佔用1個位元組,所以此處一共佔用2個位元組。

二、.word 運算式

這個運算式表示任意一節中的一個或多個運算式(同樣用逗號分開),運算式占一個字(兩個位元組)。類似的還有.long。例:.word 0x800

三、.file 字元(string)

.file 通告編譯器我們準備開啟一個新的邏輯檔。 string 是新檔案名。總的來說,檔案名是否使用引號‘"’都可以;但如果您希望規定一個空檔案名時,必須使用引號""。本語
句將來可能不再使用—允許使用它只是為了與舊版本的as編譯器程式相容。在as的一些配置中,已經刪除了.file以避免與其它的彙編器衝突。例如stage1.s中:.file ”stage1.s”。

四、.text 小節(subsection)

通知as編譯器把後續語句彙編到編號為subsection的正文子段的末尾,subsection是一個純粹的運算式。如果省略了參數subsection,則使用編號為0的子段。例:.text

五 、.code16

告訴編譯器生成16位元的指令

六、.globl symbol

.globl使得連接程式(ld)能夠看到symbol,如果gemfield在局部程式中定義了symbol,那麼與這個局部程式鏈結的局部程式也能存取symbol,例:

.globl SYMBOL_NAME(idt) 定義idt為全局符號。

七、.fill repeat , size , value
repeat, size 和value都必須是純粹的運算式。本命令生成size個位元組的repeat個副本。
Repeat可以是0或更大的值。Size 可以是0或更大的值, 但即使size大於8,也被視作8,以
相容其他的彙編器。各個副本中的內容取自一個8位元組長的數。最高4個位元組為零,最低的
4個位元組是value,它以as正在彙編的目標電腦的整數位元組順序排列。每個副本中的size
個位元組都取值於這個數最低的size個位元組。再次說明,這個古怪的動作只是為了相容其他
的彙編器。size參數和value參數是可選的。如果不存在第2個逗號和value參數,則假定value為零。如果不存在第1個逗號和其後的參數,則假定size為1。

例如,在linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:

.fill NR_CPUS*4,8,0,意思是.fill給每個cpu留有存放4個描述符的位置,並且每個描述符是8個位元組。

等等,不一而足。不過要注意的是,這種包含程式已初始化資料的節(.data)和包含程式程式還未初始化的資料的節(.bss),編譯器會把它們在4位元組上對齊,例如,.data是25位元組,那麼編譯器會將它放在28個位元組上。.

當這種以尾碼名.s編寫的A T&T格式的彙編代碼完成後,就是編譯和鏈結了。linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld:

as filename.s –o filename.o

ld filename.o –o filename 最終將源代碼轉換為目標檔.o再連接為可執行檔filename

另一種就是大名鼎鼎的被gemfield提到過的gcc:

gcc –o gemfield gemfield.S

根源程式gemfield.S的尾碼名可以使用大寫,是因為這樣可以使gcc自動識別組合語言程式中的c預處理命令,包括頭檔中的情況,像#include、#define 、#ifdef等。

本文少了嵌入式彙編的形式,才使得AT&T 彙編看起來井井有條,而非想像中的艱難。如果想要真正鍛煉一下這種彙編,源代碼stage1.s就是一個絕佳的習題。



本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/wangshenwq/archive/2009/10/19/4700481.aspx


AT&T的巨集指令 .p2align[wl]
.p2align[wl] abs-expr, abs-expr, abs-expr
Pad the location counter (in the current subsection) to a particular storage boundary. The first expression (which must be absolute) is the number of low-order zero bits the location counter must have after advancement. For example `.p2align 3' advances the location counter until it a multiple of 8. If the location counter is already a multiple of 8, no change is needed.
The second expression (also absolute) gives the fill value to be stored in the padding bytes. It (and the comma) may be omitted. If it is omitted, the padding bytes are normally zero. However, on some systems, if the section is marked as containing code and the fill value is omitted, the space is filled with no-op instructions.
The third expression is also absolute, and is also optional. If it is present, it is the maximum number of bytes that should be skipped by this alignment directive. If doing the alignment would require skipping more bytes than the specified maximum, then the alignment is not done at all. You can omit the fill value (the second argument) entirely by simply using two commas after the required alignment; this can be useful if you want the alignment to be filled with no-op instructions when appropriate.
The .p2alignw and .p2alignl directives are variants of the .p2align directive. The .p2alignw directive treats the fill pattern as a two byte word value. The .p2alignl directives treats the fill pattern as a four byte longword value. For example, .p2alignw 2,0x368d will align to a multiple of 4. If it skips two bytes, they will be filled in with the value 0x368d (the exact placement of the bytes depends upon the endianness of the processor). If it skips 1 or 3 bytes, the fill value is undefined.

Gcc嵌入式彙編
在Linux的源代碼中,有很多C語言的函數中嵌入一段組合語言程式段,這就是gcc提供的“asm”功能,例如在include/asm-i386/system.h中定義的,讀控制寄存器CR0的一個宏read_cr0(): #define read_cr0() ({ \
unsigned int __dummy; \
__asm__( \
"movl %%cr0,%0\n\t" \
"=r" (__dummy)); \
__dummy; \
}) 這種形式看起來比較陌生,這是因為這不是標準C所定義的形式,而是gcc 對C語言的擴充。其中__dummy為C函數所定義的變數;關鍵字__asm__表示彙編代碼的開始。括弧中第一個引號中為彙編指令movl,緊接著有一個冒號,這種形式閱讀起來比較複雜。

一般而言,嵌入式組合語言片段比單純的組合語言代碼要複雜得多,因為這裏存在怎樣分配和使用寄存器,以及把C代碼中的變數應該存放在哪個寄存器中。為了達到這個目的,就必須對一般的C語言進行擴充,增加對編譯器的指導作用,因此,嵌入式彙編看起來晦澀而難以讀懂。

1. 嵌入式彙編的一般形式:

__asm__ __volatile__ ("" : output : input : modify);

其中,__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項),其含義是避免“asm”指令被刪除、移動或組合;然後就是小括弧,括弧中的內容是我們介紹的重點:

· ""為彙編指令部分,例如,"movl %%cr0,%0\n\t"。數位前加首碼“%“,如%1,%2等表示使用寄存器的樣板運算元。可以使用的運算元總數取決於具體CPU中通用寄存器的數量,如Intel可以有8個。指令中有幾個運算元,就說明有幾個變數需要與寄存器結合,由gcc在編譯時根據後面輸出部分和輸入部分的約束條件進行相應的處理。由於這些樣板運算元的首碼使用了”%“,因此,在用到具體的寄存器時就在前面加兩個“%”,如%%cr0。

· 輸出部分(output),用以規定對輸出變數(目標運算元)如何與寄存器結合的約束(constraint),輸出部分可以有多個約束,互相以逗號分開。每個約束以“=”開頭,接著用一個字母來表示運算元的類型,然後是關於變數結合的約束。例如,上例中:

:"=r" (__dummy)

“=r”表示相應的目標運算元(指令部分的%0)可以使用任何一個通用寄存器,並且變數__dummy 存放在這個寄存器中,但如果是:

:“=m”(__dummy)

“=m”就表示相應的目標運算元是存放在記憶體單元__dummy中。

表示約束條件的字母很多,表 2-5 給出幾個主要的約束字母及其含義:

表2.5 主要的約束字母及其含義

· 輸入部分(Input):輸入部分與輸出部分相似,但沒有“=”。如果輸入部分一個運算元所要求使用的寄存器,與前面輸出部分某個約束所要求的是同一個寄存器,那就把對應運算元的編號(如“1”,“2”等)放在約束條件中,在後面的例子中,我們會看到這種情況。
· 修改部分(modify):這部分常常以“memory”為約束條件,以表示操作完成後記憶體中的內容已有改變,如果原來某個寄存器的內容來自記憶體,那麼現在記憶體中這個單元的內容已經改變。

注意,指令部分為必選項,而輸入部分、輸出部分及修改部分為可選項,當輸入部分存在,而輸出部分不存在時,分號“:“要保留,當“memory”存在時,三個分號都要保留,例如system.h中的巨集定義__cli():

#define __cli() __asm__ __volatile__("cli": : :"memory")

2. Linux源代碼中嵌入式彙編舉例

Linux源代碼中,在arch目錄下的.h和.c檔中,很多檔都涉及嵌入式彙編,下面以system.h中的C函數為例,說明嵌入式彙編的應用。

(1)簡單應用

#define __save_flags(x) __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */)
#define __restore_flags(x) __asm__ __volatile__("pushl %0 ; popfl": /* no output */
:"g" (x):"memory", "cc") 第一個巨集是保存標誌寄存器的值,第二個巨集是恢復標誌寄存器的值。第一個巨集中的pushfl指令是把標誌寄存器的值壓棧。而popl是把棧頂的值(剛壓入棧的flags)彈出到x變數中,這個變數可以存放在一個寄存器或記憶體中。這樣,你可以很容易地讀懂第二個宏。 (2) 較複雜應用

static inline unsigned long get_limit(unsigned long segment)
{
unsigned long __limit;
__asm__("lsll %1,%0"
:"=r" (__limit):"r" (segment));
return __limit+1;
} 這是一個設置段界限的函數,彙編代碼段中的輸出參數為__limit(即%0),輸入參數為segment(即%1)。Lsll是載入段界限的指令,即把segment段描述符中的段界限欄位裝入某個寄存器(這個寄存器與__limit結合),函數返回__limit加1,即段長。

(3)複雜應用

在Linux內核代碼中,有關字串操作的函數都是通過嵌入式彙編完成的,因為內核及用戶程式對字串函數的調用非常頻繁,因此,用彙編代碼實現主要是為了提高效率(當然是以犧牲可讀性和可維護性為代價的)。在此,我們僅列舉一個字串比較函數strcmp,其代碼在arch/i386/string.h中。

static inline int strcmp(const char * cs,const char * ct)
{
int d0, d1;
register int __res;
__asm__ __volatile__(
"1:\tlodsb\n\t"
"scasb\n\t"
"jne 2f\n\t"
"testb %%al,%%al\n\t"
"jne 1b\n\t"
"xorl %%eax,%%eax\n\t"
"jmp 3f\n"
"2:\tsbbl %%eax,%%eax\n\t"
"orb $1,%%al\n"
"3:"
:"=a" (__res), "=&S" (d0), "=&D" (d1)
:"1" (cs),"2" (ct));
return __res;
} 其中的“\n”是換行符,“\t”是tab符,在每條命令的結束加這兩個符號,是為了讓gcc把嵌入式彙編代碼翻譯成一般的彙編代碼時能夠保證換行和留有一定的空格。例如,上面的嵌入式彙編會被翻譯成:
1: lodsb //裝入串運算元,即從[esi]傳送到al寄存器,然後esi指向串中下一個元素
scasb //掃描串運算元,即從al中減去es:[edi],不保留結果,只改變標誌
jne2f //如果兩個字元不相等,則轉到標號2
testb %al %al
jne 1b
xorl %eax %eax
jmp 3f
2: sbbl %eax %eax
orb $1 %al
3:
這段代碼看起來非常熟悉,讀起來也不困難。其中1f 表示往前(forword)找到第一個標號為1的那一行,相應地,1b表示往後找。其中嵌入式彙編代碼中輸出和輸入部分的結合情況為:
· 返回值__res,放在al寄存器中,與%0相結合;
· 局部變數d0,與%1相結合,也與輸入部分的cs參數相對應,也存放在寄存器ESI中,即ESI中存放源字串的起始位址。
· 局部變數d1,與%2相結合,也與輸入部分的ct參數相對應,也存放在寄存器EDI中,即EDI中存放目的字串的起始位址。
通過對這段代碼的分析我們應當體會到,萬變不利其本,嵌入式彙編與一般彙編的區別僅僅是形式,本質依然不變。因此,全面掌握Intel 386 彙編指令乃突破閱讀低層代碼之根本。
Intel386彙編指令摘要
在閱讀Linux源代碼時,你可能遇到很多彙編指令,有些是你熟悉的,有些可能不熟悉,在此簡要列出一些常用的386彙編指令及其功能。
1. 位元操作指令
指令 功能
BT 位測試
BTC 位測試並求反
BTR 位測試並復位
BTS 位測試並置位
2.控制轉移類指令
指令 功能
CALL 調用過程
JMP 跳轉
LOOP 用ECX計數器的迴圈
LOOPNZ/LOOPNE 用ECX計數器且不為0的迴圈/用ECX計數器且不等的迴圈
RET 返回
3. 資料傳輸指令
指令 功能
IN 從埠輸入
LEA 裝入有效位址
MOV 傳送
OUT 從段口輸出
POP 從堆疊中彈出
POPA/POPAD 從棧彈出至所有寄存器
PUSH 壓棧
PUSH/PUSHAD 所有通用寄存器壓棧
XCHG 交換
4.標誌控制類指令
指令 功能
CLC 清0進位元標誌
CLD 清0方向標誌
CLI 清0中斷標誌
LAHF 將標誌寄存器裝入AH寄存器
POPF/POPFD 從棧中彈出至標誌位元
PUSHF/PUSHFD 將標誌壓棧
SAHF 將AH寄存器存入標誌寄存器
STC 置進位元標誌
STD 置方向標誌
STI 置中斷標誌
5.邏輯類指令
指令 功能
NOT 與
AND 非
OR 或
SAL/SHL 算術作移/邏輯左移
SAR 算術右移
SHLD 邏輯右移
TEST 邏輯比較
XOR 異或
6.串操作指令
指令 功能
CMPS/CMPSB/CMPSW/CMPSD 比較串運算元
INS/INSB/INSW/INSD 輸入串運算元
LODS/LODSB/LODSW/LODSD 裝入串運算元
MOVS/MOVSB/MOVSW/MOVSD 傳送串運算元
REP 重複
REPE/REPZ 相等時重複/為0時重複
SCAS/SCASB/SCASW/SCASD 掃描串運算元
STOS/STOSB/STOSW/STOSD 存儲串運算元
7.多段類操作指令
指令 功能
CALL 程序呼叫
INT 中斷過程的調用
INTO 溢出中斷過程的調用
IRET 中斷返回
JMP 跳轉
LDS 將指針轉入DS
LES 將指針轉入ES
LFS 將指針轉入FS
LGS 將指針轉入GS
LSS 將指針轉入SS
MOV 段寄存器的傳送
POP 從棧彈出至段寄存器
PUSH 壓棧
RET 返回

8.作業系統類指令
指令 功能
APPL 調整請求特權級
ALTS 清任務切換標誌
HLT 暫停
LAR 載入訪問權
LGDT 載入全局描述符表
LIDT 載入中斷描述符表
LLDT 載入局部描述符表
LMSW 載入機器狀態字
LSL 載入段界限
LTR 載入任務寄存器
MOV 特殊寄存器的資料傳送
SGDT 存儲全局描述符表
SIDT 存儲中斷描述符表
SMSW 存儲機器狀態字
STR 存儲任務寄存器


第六講 Linux中的組合語言
Linux中的組合語言
在閱讀Linux源代碼時,你可能碰到一些組合語言片段,有些組合語言出現在以.S為副檔名的彙編檔中,在這種檔中,整個程式全部由組合語言組成。有些彙編命令出現在以.c為副檔名的C檔中,在這種檔中,既有 C語言,也有組合語言,我們把出現在C代碼中的組合語言叫所“嵌入式”彙編。 不管這些彙編代碼出現在哪里,它在一定程度上都成為閱讀源代碼的攔路虎。
儘管C語言已經成為編寫作業系統的主要語言,但是,在作業系統與硬體打交道的過程中,在需要頻繁調用的函數中以及某些特殊的場合中,C語言顯得力不從心,這時,繁瑣但又高效的組合語言必須粉墨登場。 因此,在瞭解一些硬體的基礎上,必須對相關的組合語言知識也所有瞭解。
讀者可能有過在DOS作業系統下編寫組合語言程式的經歷,也具備一定的彙編知識。但是,在 Linux 的源代碼中,你可能看到了與 Intel的組合語言格式不一樣的形式,這就是AT&T的386組合語言。
一、AT&T與Intel組合語言的比較
我們知道,Linux是Unix家族的一員,儘管Linux的歷史不長,但與其相關的很多事情都發源於Unix。 就Linux所使用的386組合語言而言,它也是起源於Unix。 Unix最初是為PDP-11開發的,曾先後被移植到VAX及68000系列的處理器上,這些處理器上的彙編語言都採用的是AT&T的指令格式。當Unix被移植到i386時,自然也就採用了AT&T的匯編語言格式,而不是Intel的格式。儘管這兩種組合語言在語法上有一定的差異,但所基於的硬體知識是相同的,因此,如果你非常熟悉 Intel的語法格式,那麼你也可以很容易地把它“移植“到AT&T來。下面我們通過對照Intel與AT&T的語法格式,以便於你把過去的知識能很快地“移植”過來。
1.首碼
在Intel的語法中,寄存器和和立即數都沒有首碼。但是在AT&T中,寄存器前冠以“%”,而立即數前冠以“$”。在Intel的語法中,十六進位和二進位立即數尾碼分別冠以“h”和“b”,而在AT&T中,十六進位立即數前冠以“0x”,表2.2給出幾個相應的例子。
表2.2 Intel與AT&T首碼的區別
Intel語法: AT&T語法:
mov eax,8 Movl $8,%eax
mov ebx,0ffffh Movl $0xffff,%ebx
int 80h int $0x80

2. 運算元的方向
Intel與AT&T運算元的方向正好相反。在Intel語法中,第一個運算元是目的運算元,第二個運算元源運算元。而在AT&T中,第一個數是源運算元,第二個數是目的運算元。 由此可以看出,AT&T 的語法符合人們通常的閱讀習慣。
例如:
在Intel中, mov eax,[ecx] 在AT&T中,movl (%ecx),%eax

3.記憶體單元運算元
從上面的例子可以看出,記憶體運算元也有所不同。在 Intel的語法中,基寄存器用“[]”括起來,而在AT&T中,用“()”括起來。
例如:
在Intel中,mov eax,[ebx+5]
在AT&T,movl 5(%ebx),%eax
4.間接定址方式
與Intel的語法比較,AT&T間接定址方式可能更晦澀難懂一些。
Intel的指令格式是segreg:[base+index*scale+disp],而AT&T的格式是%segreg:disp(base,index,scale)。其中index/scale/disp/segreg全部是可選的,完全可以簡化掉。如果沒有指定scale而指定了index,則scale的缺省值為1。segreg段寄存器依賴于指令以及應用程式是運行在實模式還是保護模式下,在實模式下,它依賴於指令,而在保護模式下,segreg是多餘的。在AT&T中,當立即數用在scale/disp中時,不應當在其前冠以“$”首碼,表2.3給出其語法及幾個相應的例子。
表2.3 記憶體運算元的語法及舉例
Intel語法: AT&T語法:
mov eax,[ebx+20h] Movl0x20(%ebx),%eax
add eax,[ebx+ecx*2h Addl (%ebx,%ecx,0x2),%eax
lea eax,[ebx+ecx] Leal (%ebx,%ecx),%eax
sub eax,[ebx+ecx*4h-20h] Subl -0x20(%ebx,%ecx,0x4),%eax
指令 foo,segreg:[base+index*scale+disp] 指令 %segreg:disp(base,index,scale),foo

從表中可以看出,AT&T的語法比較晦澀難懂,因為[base+index*scale+disp]一眼就可以看出其含義,而disp(base,index,scale)則不可能做到這點。
這種定址方式常常用在訪問資料結構陣列中某個特定元素內的一個欄位,其中,base為陣列的起始位址,scale為每個陣列元素的大小,index為下標。如果陣列元素還是一個結構,則disp為具體欄位在結構中的位移。
5.操作碼的尾碼
在上面的例子中你可能已注意到,在AT&T的操作碼後面有一個尾碼,其含義就是指出操作碼的大小。“l”表示長整數(32位元),“w”表示字(16位元),“b”表示位元組(8位)。 而在Intel的語法中,則要在記憶體單元運算元的前面加上byte ptr、 word ptr,和dword ptr,“dword”對應“long”。表2.4給出幾個相應的例子。
表2.4 操作碼的尾碼舉例
Intel語法 AT&T語法
Mov al,bl movb %bl,%al
Mov ax,bx movw %bx,%ax
Mov eax,ebx movl %ebx,%eax
Mov eax, dword ptr [ebx] movl (%ebx),%eax

二、 AT&T組合語言的相關知識
在Linux源代碼中,以.S為副檔名的文件是“純”組合語言的文件。這裏,我們結合具體的例子再介紹一些AT&T組合語言的相關知識。
1.GNU組合語言程式GAS GNU Assembly和連接程式當你編寫了一個程式後,就需要對其進行彙編(assembly)和連接。在Linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld,一種是使用gcc。我們先來看一下GAS和ld:
GAS把組合語言原始檔案(.o)轉換為目標檔(.o),其基本語法如下:
as filename.s -o filename.o
一旦創建了一個目標檔,就需要把它連接並執行,連接一個目標檔的基本語法為:
ld filename.o -o filename
這裏 filename.o是目標檔案名,而filename 是輸出(可執行) 檔。GAS使用的是AT&T的語法而不是Intel的語法,這就再次說明了AT&T語法是Unix世界的標準,你必須熟悉它。如果要使用GNC的C編譯器gcc,就可以一步完成彙編和連接,例如:
gcc -o example example.S
這裏,example.S是你的組合語言程式,輸出檔(可執行檔)名為example。其中,擴展名必須為大寫的S,這是因為,大寫的S可以使gcc自動識別組合語言程式中的C預處理命令,像#include、 #define、 #ifdef、 #endif等,也就是說,使用gcc進行編譯,你可以在組合語言程式中使用C的預處理命令。
2. AT&T中的節(Section)
在AT&T的語法中,一個節由.section關鍵字來標識,當你編寫組合語言程式時,至少需要有以下三種節:

.section .data: 這種節包含程式已初始化的資料,也就是說,包含具有初值的那些變量,例如:
hello : .string "Hello world!\n"
hello_len : .long 13
.section .bss:這個節包含程式還未初始化的資料,也就是說,包含沒有初值的那些變量。當操作 系統裝入這個程式時將把這些變數都置為0,例如:
name : .fill 30 # 用來請求用戶輸入名字
name_len : .long 0 # 名字的長度 (尚未定義)
當這個程式被裝入時,name 和 name_len都被置為0。如果你在.bss節不小心給一個變數賦了初值,這個值也會丟失,並且變數的值仍為0。
使用.bss比使用.data的優勢在於,.bss節不佔用磁片的空間。在磁片上,一個長整數就足以存放.bss節。當程式被裝入到記憶體時,作業系統也只分配給這個節4個位元組的記憶體大小。 注意:編譯程序把.data和.bss在4位元組上對齊(align),例如,.data總共有34字節,那麼編譯程序把它對其在36位元組上,也就是說,實際給它36位元組的空間。
.section .text :這個節包含程式的代碼,它是唯讀節,而.data 和.bss是讀/寫節。
3.組合語言程式指令(Assembler Directive)
上面介紹的.section就是組合語言程式指令的一種,GNU組合語言程式提供了很多這樣的指令(directiv),這種指令都是以句點(.)為開頭,後跟指令名(小寫字母),在此,我們只介紹在內核源代碼中出現的幾個指令(以arch/i386/kernel/head.S中的代碼為例)。
(1)ascii "string"...
.ascii 表示零個或多個(用逗號隔開)字串,並把每個字串(結尾不自動加“0“位元組)中的字元放在連續的位址單元。
還有一個與.ascii類似的.asciz,z代表“0“,即每個字串結尾自動加一個”0“位元組,例如:
int_msg:
.asciz "Unknown interrupt\n"
(2).byte 運算式
.byte表示零或多個運算式(用逗號隔開),每個運算式被放在下一個位元組單元。
(3).fill 運算式
形式:.fill repeat , size , value
其中,repeat、 size 和value都是常量運算式。 Fill的含義是反復拷貝size個位元組。
Repeat可以大於等於0。 size也可以大於等於0,但不能超過8,如果超過8,也只取8。把repeat個位元組以8個為一組,每組的最高4個位元組內容為0,最低4位元組內容置為value。 Size和 value為可選項。如果第二個逗號和value值不存在,則假定value為0。如果第一個逗號和size不存在,則假定size為1。
例如,在Linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:
.fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */
因為每個描述符正好占 8個位元組,因此,.fill給每個CPU留有存放4個描述符的位置。
(4).globl symbol
.globl使得連接程式(ld)能夠看到symbl。如果你的局部程式中定義了symbl,那麼,與這個局部程式連接的其他局部程式也能存取symbl,例如:
.globl SYMBOL_NAME(idt)
.globl SYMBOL_NAME(gdt)
定義idt和gdt為全局符號。
(5)quad bignums
.quad表示零個或多個bignums(用逗號分隔),對於每個bignum,其缺省值是8字節整數。如果bignum超過8位元組,則列印一個警告資訊;並只取bignum最低8位元組。例如,對全局描述符表的填充就用到這個指令:
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
(6)rept count
把.rept指令與.endr指令之間的行重複count次,例如
.rept 3 .long 0
.endr
相當於
.long 0
.long 0
.long 0
(7)space size , fill
這個指令保留size個位元組的空間,每個位元組的值為fill。 size 和fill都是常量運算式。
如果逗號和fill被省略,則假定fill為0,例如在arch/i386/bootl/setup.S中有一句:
.space 1024
表示保留1024位元組的空間,並且每個位元組的值為0。
(8).word expressions
這個運算式表示任意一節中的一個或多個運算式(用逗號分開),運算式的值占兩個位元組,例如:
gdt_descr:
.word GDT_ENTRIES*8-1
表示變數gdt_descr的置為GDT_ENTRIES*8-1
(9).long expressions
這與.word類似
(10).org new-lc , fill
把當前節的位置計數器提前到new-lc(new location counter)。 new-lc或者是一個常量運算式,或者是一個與當前子節處於同一節的運算式。也就是說,你不能用.org橫跨節:如果new-lc是個錯誤的值,則.org被忽略。 .org只能增加位置計數器的值,或者讓其保持不變;但絕不能用.org來讓位置計數器倒退。 注意,位置計數器的起始值是相對於一個節的開始的,而不是子節的開始。當位置計數器被提升後,中間位置的位元組被填充值 fill(這也是一個常量運算式)。如果逗號和fill都省略,則fill的缺省值為0。 例如:.org 0x2000
ENTRY(pg0)
表示把位置計數器置為0x2000,這個位置存放的就是臨時頁表pg0。
三、 Gcc嵌入式彙編
在Linux的源代碼中,有很多C語言的函數中嵌入一段組合語言程式段,這就是 gcc提供的“asm”功能,例如在 include/asm-i386/system.h中定義的,讀控制寄存器 CR0的一個宏read_cr0():
#define read_cr0() ({ \
unsigned int __dummy; \
__asm__( \
"movl %%cr0,%0\n\t" \
:"=r" (__dummy)); \
__dummy; \
})
這種形式看起來比較陌生,這是因為這不是標準 C所定義的形式,而是gcc 對C語言的擴充。其中__dummy為C函數所定義的變數;關鍵字__asm__表示彙編代碼的開始。 括弧中第一個引號中為彙編指令movl,緊接著有一個冒號,這種形式閱讀起來比較複雜。一般而言,嵌入式組合語言片段比單純的組合語言代碼要複雜得多,因為這裏存在怎樣分配和使用寄存器,以及把C代碼中的變數應該存放在哪個寄存器中。 為了達到這個目的,就必須對一般的C語言進行擴充,增加對編譯器的指導作用,因此,嵌入式彙編看起來晦澀而難以讀懂。
1. 嵌入式彙編的一般形式:
__asm__ __volatile__ ("" : output : input : modify);
其中,__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項),其含義是避免“asm”指令被刪除、移動或組合;然後就是小括弧,括弧中的內容是我們介紹的重點:
· ""為彙編指令部分,例如,"movl %%cr0,%0\n\t"。數位前加首碼“%“,如%1,%2等表示使用寄存器的樣板運算元。可以使用的運算元總數取決於具體CPU中通用寄存器的數量,如Intel可以有8個。指令中有幾個運算元,就說明有幾個變量需要與寄存器結合,由gcc在編譯時根據後面輸出部分和輸入部分的約束條件進行相應的處理。 由於這些樣板運算元的首碼使用了”%“,因此,在用到具體的寄存器時就在前面加兩個“%”,如%%cr0。
· 輸出部分(output),用以規定對輸出變數(目標運算元)如何與寄存器結合的約束(constraint),輸出部分可以有多個約束,互相以逗號分開。 每個約束以“=”開頭,接著用一個字母來表示運算元的類型,然後是關於變數結合的約束。 例如,上例中:
:"=r" (__dummy)“=r”表示相應的目標運算元(指令部分的%0)可以使用任何一個通用寄存器,並且變數__dummy 存放在這個寄存器中,但如果是:
:“=m”(__dummy)
“=m”就表示相應的目標運算元是存放在記憶體單元__dummy中。
表示約束條件的字母很多,表 2-5 給出幾個主要的約束字母及其含義:
表2.5 主要的約束字母及其含義
字母: 含義:
m, v,o 表示記憶體單元
R 表示任何通用寄存器
Q 表示寄存器eax, ebx, ecx,edx之一
I, h 表示直接運算元
E, F 表示浮點數
G 表示“任意”
a, b.c d 表示要求使用寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cl或edx/dx/dl
S, D 表示要求使用寄存器esi或edi
I 表示常數(0~31)
· 輸入部分(Input):輸入部分與輸出部分相似,但沒有“=”。如果輸入部分一個運算元所要求使用的寄存器,與前面輸出部分某個約束所要求的是同一個寄存器,那就把對應運算元的編號(如“1”,“2”等)放在約束條件中,在後面的例子中我們會看到這種情況。
· 修改部分(modify):這部分常常以“memory”為約束條件,以表示操作完成後記憶體中的內容已有改變,如果原來某個寄存器的內容來自記憶體,那麼現在記憶體中這個單元的內容已經改變。注意,指令部分為必選項,而輸入部分、輸出部分及修改部分為可選項,當輸入部分存在,而輸出部分不存在時,分號“:“要保留,當“memory”存在時,三個分號都要保留,例如system.h中的巨集定義__cli(): #define __cli() __asm__ __volatile__("cli": : :"memory")
2. Linux源代碼中嵌入式彙編舉例
Linux源代碼中,在arch目錄下的.h和.c檔中,很多檔都涉及嵌入式彙編,下面以system.h中的C函數為例,說明嵌入式彙編的應用。
(1)簡單應用
#define __save_flags(x) __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */)
#define __restore_flags(x) __asm__ __volatile__("pushl %0 ; popfl": /* no output */ :"g" (x):"memory", "cc")
第一個巨集是保存標誌寄存器的值,第二個巨集是恢復標誌寄存器的值。第一個宏中的pushfl指令是把標誌寄存器的值壓棧。 而popl是把棧頂的值(剛壓入棧的flags)彈出到x變數中,這個變數可以存放在一個寄存器或記憶體中。這樣,你可以很容易地讀懂第二個宏。
(2) 較複雜應用
static inline unsigned long get_limit(unsigned long segment)
{
unsigned long __limit;
__asm__("lsll %1,%0"
:"=r" (__limit):"r" (segment));
return __limit+1;
}
這是一個設置段界限的函數,彙編代碼段中的輸出參數為__limit(即%0),輸入參數為segment(即%1)。Lsll是載入段界限的指令,即把segment段描述符中的段界限字段裝入某個寄存器(這個寄存器與__limit結合),函數返回__limit加1,即段長。(3)複雜應用在Linux內核代碼中,有關字串操作的函數都是通過嵌入式彙編完成的,因為內核及用戶程式對字串函數的調用非常頻繁,因此,用彙編代碼實現主要是為了提高效率(當然是以犧牲可讀性和可維護性為代價的)。在此,我們僅列舉一個字串比較函數strcmp,其代碼在arch/i386/string.h中。
static inline int strcmp(const char * cs,const char * ct)
{
int d0, d1;
register int __res;
__asm__ __volatile__(
"1:\tlodsb\n\t"
"scasb\n\t"
"jne 2f\n\t"
"testb %%al,%%al\n\t"
"jne 1b\n\t"
"xorl %%eax,%%eax\n\t"
"jmp 3f\n"
"2:\tsbbl %%eax,%%eax\n\t"
"orb $1,%%al\n"
"3:"
:"=a" (__res), "=&S" (d0), "=&D" (d1)
:"1" (cs),"2" (ct));
return __res;
}
其中的“\n”是換行符,“\t”是tab符,在每條命令的結束加這兩個符號,是為了讓gcc把嵌入式彙編代碼翻譯成一般的彙編代碼時能夠保證換行和留有一定的空格。例如,上面的嵌入式彙編會被翻譯成:
1: lodsb //裝入串運算元,即從[esi]傳送到al寄存器,然後esi指向串中下一個元素
scasb //掃描串運算元,即從al中減去es:[edi],不保留結果,只改變標誌
jne2f //如果兩個字元不相等,則轉到標號2
testb %al %al
jne 1b
xorl %eax %eax
jmp 3f
2: sbbl %eax %eax
orb $1 %al
3:
這段代碼看起來非常熟悉,讀起來也不困難。其中1f 表示往前(forword)找到第一個標號為1的那一行,相應地,1b表示往後找。其中嵌入式彙編代碼中輸出和輸入部分的結合情況為:
· 返回值__res,放在al寄存器中,與%0相結合;· 局部變數d0,與%1相結合,也與輸入部分的cs參數相對應,也存放在寄存器ESI中,即ESI中存放源字串的起始位址。· 局部變數d1, 與%2相結合,也與輸入部分的ct參數相對應,也存放在寄存器EDI中,即EDI中存放目的字串的起始位址。
通過對這段代碼的分析我們應當體會到,萬變不利其本,嵌入式彙編與一般彙編的區別僅僅是形式,本質依然不變。因此,全面掌握Intel 386 彙編指令乃突破閱讀低層代碼之根本。

http://book.csdn.net/bookfiles/824/10082424777.shtml

AT&T 組合語言指令詳解

AT&T 組合語言指令詳解
以下資料皆為網路搜尋的;
在閱讀linux/unix內核源代碼的時候,必須先掌握彙編,大家都知道,內核代碼用的編譯器是gcc,而gcc採用的是AT&T的彙編格式,與MS的intel有些區別。
一 AT&T的基本語法
語法上主要有以下幾個不同.
★ 寄存器命名原則
AT&T: %eax Intel: eax
★ 源/目的操作數順序
AT&T: movl %eax,%ebx Intel: mov ebx,eax
★ 常數/立即數的格式
AT&T: movl $_value,%ebx Intel: mov eax,_value
把_value的位址放入eax寄存器
AT&T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d
★ 操作數長度標識
AT&T: movw %ax,%bx Intel: mov bx,ax
★尋址方式
AT&T: immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + imm32)
Linux工作於保護模式下,用的是32位線性位址,所以在計算位址時
不用考慮segment-offset的問題.上式中的地址應為:
imm32 + basepointer + indexpointer*indexscale
下面是一些例子:
★直接尋址
AT&T: _booga ; _booga是一個全局的C變量
注意加上$是表示位址引用,不加是表示值引用.
註:對於局部變量,可以通過堆棧指針引用.
Intel: [_booga]
★寄存器間接尋址
AT&T: (%eax)
Intel: [eax]
★變址尋址
AT&T: _variable(%eax)
Intel: [eax + _variable]
AT&T: _array(,%eax,4)
Intel: [eax*4 + _array]
AT&T: _array(%ebx,%eax,8)
Intel: [ebx + eax*8 + _array]

二 基本的行內彙編

基本的行內彙編很簡單,一般是按照下面的格式
asm("statements");
例如:asm("nop"); asm("cli");
asm 和 __asm__是完全一樣的.
如果有多行彙編,則每一行都要加上 "\n\t"
例如:
asm( "pushl %eax\n\t"
"movl $0,%eax\n\t"
"popl %eax");
實際上gcc在處理彙編時,是要把asm(...)的內容"列印"到彙編
檔中,所以格式控制字元是必要的.
再例如:
asm("movl %eax,%ebx");
asm("xorl %ebx,%edx");
asm("movl $0,_booga);
在上面的例子中,由於我們在行內彙編中改變了edx和ebx的值,但是
由於gcc的特殊的處理方法,即先形成彙編檔,再交給GAS去彙編,
所以GAS並不知道我們已經改變了edx和ebx的值,如果程式的上下文
需要edx或ebx作暫存,這樣就會引起嚴重的後果.對於變量_booga也
存在一樣的問題.為瞭解決這個問題,就要用到擴展的行內彙編語法.

三 擴展的行內彙編

擴展的行內彙編類似於Watcom.
基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是會被改變的寄存器.
下面是一個例子(為方便起見,我使用全局變量):
int count=1;
int value=1;
int buf[10];
void main()
{
asm(
"cld \n\t"
"rep \n\t"
"stosl"
:
: "c" (count), "a" (value) , "D" (buf[0])
: "%ecx","%edi" );
}
得到的主要彙編代碼為:
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos就不用多解釋了.
這幾條語句的功能是向buf中寫上count個value值.
冒號後的語句指明輸入,輸出和被改變的寄存器.
通過冒號以後的語句,編譯器就知道你的指令需要和改變哪些寄存器,
從而可以優化寄存器的分配.
其中符號"c"(count)指示要把count的值放入ecx寄存器
類似的還有:
a eax
b ebx
c ecx
d edx
S esi
D edi
I 常數值,(0 - 31)
q,r 動態分配的寄存器
g eax,ebx,ecx,edx或內存變量
A 把eax和edx合成一個64位的寄存器(use long longs)
我們也可以讓gcc自己選擇合適的寄存器.
如下面的例子:
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) );
這段代碼實現5*x的快速乘法.
得到的主要彙編代碼為:
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x
幾點說明:
1.使用q指示編譯器從eax,ebx,ecx,edx分配寄存器.
使用r指示編譯器從eax,ebx,ecx,edx,esi,edi分配寄存器.
2.我們不必把編譯器分配的寄存器放入改變的寄存器列表,因為寄存器
已經記住了它們.
3."="是標示輸出寄存器,必須這樣用.
4.數字%n的用法:
數字表示的寄存器是按照出現和從左到右的順序映射到用"r"或"q"請求
的寄存器.如果我們要重用"r"或"q"請求的寄存器的話,就可以使用它們.
5.如果強制使用固定的寄存器的話,如不用%1,而用ebx,則
asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) );
注意要使用兩個%,因為一個%的語法已經被%n用掉了.
下面可以來解釋letter 4854-4855的問題:
1、變量加下劃線和雙下劃線有什麼特殊含義嗎?
加下劃線是指全局變量,但我的gcc中加不加都無所謂.
2、以上定義用如下調用時展開會是什麼意思?
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
/* __res應該是一個全局變量 */
__asm__ volatile ("int $0x80" \
/* volatile 的意思是不允許優化,使編譯器嚴格按照你的彙編代碼彙編*/
: "=a" (__res) \
/* 產生代碼 movl %eax, __res */
: "0" (__NR_##name),"b" ((long)(arg1))); \
/* 如果我沒記錯的話,這裡##指的是兩次宏展開.
  即用實際的系統調用名字代替"name",然後再把__NR_...展開.
  接著把展開的常數放入eax,把arg1放入ebx */
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}

四.AT&T彙編與Intel彙編的比較

Intel和AT&T語法的區別

Intel和AT&T彙編語言的語法表面上各不相同,這將導致剛剛學會INTEL彙編的人第一次見到AT&T彙編時
會感到困惑,或者反之。因此讓我們從基礎的東西開始。
前綴
在Intel彙編中沒有寄存器前綴或者立即數前綴。而在AT&T彙編中寄存器有一個「%」前綴,立即數有
一個「$」前綴。Intel語句中十六進制和二進制數據分別帶有「h」和「b」後綴,並且如果十六進制
數字的第一位是字母的話,那麼數值的前面要加一個「0」前綴。
例如,
Intex Syntax
mov eax,1
mov ebx,0ffh
int 80h
AT&T Syntax
movl $1,%eax
movl $0xff,%ebx
int $0x80
就像你看到的,AT&T非常難懂。[base+index*scale+disp] 看起來比disp(base,index,scale)更好理解。
操作數的用法
intel語句中操作數的用法和AT&T中的用法相反。在Intel語句中,第一個操作數表示目的,第二個
操作數表示源。然而在AT&T語句中第一個操作數表示源而第二個操作數表示目的。在這種情形下AT&T語法
的好處是顯而易見的。我們從左向右讀,也從左向右寫,這樣比較自然。
例如,
Intex Syntax
instr dest,source
mov eax,[ecx]
AT&T Syntax
instr source,dest
movl (%ecx),%eax
存儲器操作數
如同上面所看到的,存儲器操作數的用法也不相同。在Intel語句中基址寄存器用「[」和「]」括起來
而在AT&T語句中是用「(」和「)」括起來的。
例如,
Intex Syntax
mov eax,[ebx]
mov eax,[ebx+3]
AT&T Syntax
movl (%ebx),%eax
movl 3(%ebx),%eax
AT&T語法中用來處理複雜的操作的指令的形式和Intel語法中的形式比較起來要難懂得多。在Intel語句
中這樣的形式是segreg:[base+index*scale+disp]。在AT&T語句中這樣的形式是
%segreg:disp(base,index,scale)。
Index/scale/disp/segreg 都是可選並且可以去掉的。Scale在本身沒有說明而index已指定的情況下
缺省值為1。segreg的確定依賴於指令本身以及程式運行在實模式還是pmode。在實模式下它依賴於
指令本身而pmode模式下它是不需要的。在AT&T語句中用作scale/disp的立即數不要加「$」前綴。
例如
Intel Syntax
instr foo,segreg:[base+index*scale+disp]
mov eax,[ebx+20h]
add eax,[ebx+ecx*2h]
lea eax,[ebx+ecx]
sub eax,[ebx+ecx*4h-20h]
AT&T Syntax
instr %segreg:disp(base,index,scale),foo
movl 0x20(%ebx),%eax
addl (%ebx,%ecx,0x2),%eax
leal (%ebx,%ecx),%eax
subl -0x20(%ebx,%ecx,0x4),%eax

後綴

就像你已經注意到的,AT&T語法中有一個後綴,它的意義是表示操作數的大小。「l」代表long,
「w」代表word,「b」代表byte。Intel語法中在處理存儲器操作數時也有類似的表示,
如byte ptr, word ptr, dword ptr。"dword" 顯然對應於「long」。這有點類似於C語言中定義的
類型,但是既然使用的寄存器的大小對應著假定的數據類型,這樣就顯得不必要了。
例子:
Intel Syntax
mov al,bl
mov ax,bx
mov eax,ebx
mov eax, dword ptr [ebx]
AT&T Syntax
movb %bl,%al
movw %bx,%ax
movl %ebx,%eax
movl (%ebx),%eax
注意:從此開始所有的例子都使用AT&T語法

AT&T組合語言的相關知識

在Linux源代碼中,以.S為副檔名的文件是“純”組合語言的文件。這裏,我們結合具體的例子再介紹一些AT&T組合語言的相關知識。
1.GNU組合語言程式GAS GNU Assembly和連接程式
當你編寫了一個程式後,就需要對其進行彙編(assembly)和連接。在Linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld,一種是使用gcc。我們先來看一下GAS和ld:
GAS把組合語言原始檔案(.o)轉換為目標檔(.o),其基本語法如下:

as filename.s -o filename.o

一旦創建了一個目標檔,就需要把它連接並執行,連接一個目標檔的基本語法為:
ld filename.o -o filename
這裏 filename.o是目標檔案名,而filename 是輸出(可執行) 檔。

GAS使用的是AT&T的語法而不是Intel的語法,這就再次說明了AT&T語法是Unix世界的標準,你必須熟悉它。

如果要使用GNC的C編譯器gcc,就可以一步完成彙編和連接,例如:

gcc -o example example.S

這裏,example.S是你的組合語言程式,輸出檔(可執行檔)名為example。其中,副檔名必須為大寫的S,這是因為,大寫的S可以使gcc自動識別組合語言程式中的C預處理命令,像#include、#define、#ifdef、 #endif等,也就是說,使用gcc進行編譯,你可以在組合語言程式中使用C的預處理命令。

2. AT&T中的節(Section)
在AT&T的語法中,一個節由.section關鍵字來標識,當你編寫組合語言程式時,至少需要有以下三種節:
.section .data: 這種節包含程式已初始化的資料,也就是說,包含具有初值的那些變數,例如:
hello : .string "Hello world!\n"

hello_len : .long 13

.section .bss:這個節包含程式還未初始化的資料,也就是說,包含沒有初值的那些變數。當操作

系統裝入這個程式時將把這些變數都置為0,例如:

name : .fill 30 # 用來請求用戶輸入名字

name_len : .long 0 # 名字的長度 (尚未定義)

當這個程式被裝入時,name 和 name_len都被置為0。如果你在.bss節不小心給一個變數賦了初值,這個值也會丟失,並且變數的值仍為0。

使用.bss比使用.data的優勢在於,.bss節不佔用磁片的空間。在磁片上,一個長整數就足以存放.bss節。當程式被裝入到記憶體時,作業系統也只分配給這個節4個位元組的記憶體大小。

注意:編譯程序把.data和.bss在4位元組上對齊(align),例如,.data總共有34位元組,那麼編譯程序把它對其在36位元組上,也就是說,實際給它36位元組的空間。

.section .text :這個節包含程式的代碼,它是唯讀節,而.data 和.bss是讀/寫節。

3.組合語言程式指令(Assembler Directive)

上面介紹的.section就是組合語言程式指令的一種,GNU組合語言程式提供了很多這樣的指令(directiv),這種指令都是以句點(.)為開頭,後跟指令名(小寫字母),在此,我們只介紹在內核源代碼中出現的幾個指令(以arch/i386/kernel/head.S中的代碼為例)。

(1)ascii "string"...

.ascii 表示零個或多個(用逗號隔開)字串,並把每個字串(結尾不自動加“0“位元組)中的字元放在連續的位址單元。

還有一個與.ascii類似的.asciz,z代表“0“,即每個字串結尾自動加一個”0“位元組,例如:

int_msg:

.asciz "Unknown interrupt\n"

(2).byte 運算式

.byte表示零或多個運算式(用逗號隔開),每個運算式被放在下一個位元組單元。

(3).fill 運算式

形式:.fill repeat , size , value

其中,repeat、size 和value都是常量運算式。Fill的含義是反復拷貝size個位元組。Repeat可以大於等於0。size也可以大於等於0,但不能超過8,如果超過8,也只取8。把repeat個位元組以8個為一組,每組的最高4個位元組內容為0,最低4位元組內容置為value。

Size和 value為可選項。如果第二個逗號和value值不存在,則假定value為0。如果第一個逗號和size不存在,則假定size為1。

例如,在Linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:

.fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */

因為每個描述符正好占8個位元組,因此,.fill給每個CPU留有存放4個描述符的位置。

(4).globl symbol

.globl使得連接程式(ld)能夠看到symbl。如果你的局部程式中定義了symbl,那麼,與這個局部程式連接的其他局部程式也能存取symbl,例如:

.globl SYMBOL_NAME(idt)

.globl SYMBOL_NAME(gdt)

定義idt和gdt為全局符號。


(5)quad bignums

.quad表示零個或多個bignums(用逗號分隔),對於每個bignum,其缺省值是8位元組整數。如果bignum超過8位元組,則列印一個警告資訊;並只取bignum最低8位元組。

例如,對全局描述符表的填充就用到這個指令:

.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */

.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */

.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */

.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */


(6)rept count

把.rept指令與.endr指令之間的行重複count次,例如

.rept 3

.long 0

.endr

相當於

.long 0

.long 0

.long 0

(7)space size , fill


這個指令保留size個位元組的空間,每個位元組的值為fill。size 和fill都是常量運算式。如果逗號和fill被省略,則假定fill為0,例如在arch/i386/bootl/setup.S中有一句:

.space 1024

表示保留1024位元組的空間,並且每個位元組的值為0。

(8).word expressions

這個運算式表示任意一節中的一個或多個運算式(用逗號分開),運算式的值占兩個位元組,例如:

gdt_descr:

.word GDT_ENTRIES*8-1

表示變數gdt_descr的置為GDT_ENTRIES*8-1

(9).long expressions

這與.word類似

(10).org new-lc , fill

把當前節的位置計數器提前到new-lc(new location counter)。new-lc或者是一個常量運算式,或者是一個與當前子節處於同一節的運算式。也就是說,你不能用.org橫跨節:如果new-lc是個錯誤的值,則.org被忽略。.org只能增加位置計數器的值,或者讓其保持不變;但絕不能用.org來讓位置計數器倒退。

注意,位置計數器的起始值是相對於一個節的開始的,而不是子節的開始。當位置計數器被提升後,中間位置的位元組被填充值fill(這也是一個常量運算式)。如果逗號和fill都省略,則fill的缺省值為0。

例如:.org 0x2000

ENTRY(pg0)

表示把位置計數器置為0x2000,這個位置存放的就是臨時頁表pg0。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/kpgood/archive/2009/04/05/4049495.aspx