2010年3月4日 星期四

在安裝DOS的硬碟,將MBR讀出,並反組譯



http://www.pczone.com.tw/vbb3/archive/t-51849.html這裡的code比較多,這是用光碟安裝xp時,寫入mbrㄉ狀況;但此連結也有很多可參考的資訊。以下這一小段程式可能比較適合理解mbr的Boot Partition Loader ,這是我用virtualBoxㄉ虛擬dos執行fdisk後將虛擬HD分割為單一顆後,並用spfdisk將磁註0磁頭0磁區1的MBR共512bytes儲存,並反組譯所得到的程式碼如下。
參考以下的CODE必須參照上圖。

.386
seg000 segment byte public '' use16
assume cs:seg000
assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
cli ;關閉中斷
xor ax, ax ;ax=0
mov ss, ax ;ss=0
mov sp, 7C00h ;sp=7c00h
mov si, sp ;si=7c00h
push ax
pop es ;將es設為0
push ax
pop ds ;將ds設為0
sti ;啟用中斷
cld ;清除方向旗標,使si di隨位址遞增
mov di, 600h ;設di=600h
mov cx, 100h ;設cx=100h,目的當成計數器
repne movsw ;重覆搬移100h次,每次搬移2bytes,來源ds:si -->0000:7c00h 目的es:di -->0000:0600h 因此共搬移512bytes
jmp far ptr 0:61Dh ;長跳躍到0000:061dh

mov si, 7BEh ;7beh - 600h = 1beh,也就是說si指到位移值 01BE ~ 01CD <-- 第一分割表
mov bl, 4 ;設定bl=4
next:
cmp byte ptr [si], 80h ;比對first_partition第一ㄍbyte是否為80h
jz is_a_boot_partition
cmp byte ptr [si], 0 ;比對first_partition第一ㄍbyte是否為00h
jnz invalid_table
add si, 10h ;將si指到下一ㄍ分割表
dec bl ;bl減1
jnz next
int 18h ; TRANSFER TO ROM BASIC
; causes transfer to ROM-based BASIC (IBM-PC)
; often reboots a compatible; often has no effect at all
is_a_boot_partition:
mov dx, [si] ;設dx=80h ,也就是DH=01 DL=80H 請參考first_partition
mov cx, [si+2] ;設cx=01h ,也就是CH=00 CL=01H 等一下讀取硬諜時會用到,可參考first_partition 分割開始之磁區編號 (6 bits) 最高的 2 個 bits(bit6-7), 為磁柱編號的 bit8-9
mov bp, si ;將bp設為指向first_partition

next_partition:
add si, 10h
dec bl
jz chk_set_OK
cmp byte ptr [si], 0
jz next_partition

invalid_table:
mov si, 68Bh ;68B指到str_invalid

show_msg:
lodsb ;以下幾行code是呼叫int 10h顯示單一字元
cmp al, 0
jz Exit
push si
mov bx, 7
mov ah, 0Eh
int 10h
; - VIDEO - WRITE CHARACTER AND
; AL = character, BH = display page (alpha modes)
; BL = foreground color (graphics modes)
pop si
jmp show_msg
; ---------------------------------------------------------------------------

Exit:
jmp Exit ;無窮迴圈 程式停在此處
; ---------------------------------------------------------------------------

chk_set_OK:
mov di, 5

repeat_5time:
;這個中斷程式的spec:http://lrs.uni-passau.de/support/doc/interrupt-57/RB-0569.HTM
mov bx, 7C00h ;
mov ax, 201h ;讀取1ㄍ磁區
push di
int 13h ; 讀取 磁頭head=1 drive=80H 磁軌track=0 磁區sector=1(*****此處就是dos作業系統INSTALL的啟始位置*****)
; DISK - READ SECTORS INTO MEMORY
; AL = number of sectors to read, CH = track, CL = sector
; DH = head, DL = drive, ES:BX -> buffer to fill
; Return: CF set on error, AH = status, AL = number of sectors read
pop di
jnb drive_read_OK
xor ax, ax
int 13h ; DISK - RESET DISK SYSTEM
; DL = drive (if bit 7 is set both hard disks and floppy disks reset)
dec di
jnz repeat_5time
mov si, 6A3h ;6A3h指向str_err_loading "Error loading operating system"
jmp short show_msg
; ---------------------------------------------------------------------------

drive_read_OK:
mov si, 6C2h ;6c2h指向str_miss_OS "Missing operating system"
mov di, 7DFEh ;7DFEh指向55aa之55位置
cmp word ptr [di], 0AA55h ;比對是否510及511位置是否為55aa
jnz short show_msg ;若不是55AA便顯示"Missing operating system"
mov si, bp
jmp far ptr 0:7C00h ; 轉移控制權到 0000:7C00H 程式到止結束;注意,此時的0000:7C00H已經更新為剛才重新讀取的磁頭head=1 drive=80H 磁軌track=0 磁區sector=1,因此這裡也就是進入dos作業系統的指令。
; ---------------------------------------------------------------------------
str_invalid db 'Invalid partition table',0
str_err_loading db 'Error loading operating system',0
str_miss_OS db 'Missing operating system',0
db 0
db 88h ;
db 74h ;
db 63h ;
db 3Ch ;
db 222 dup(0)
first_partition db 80h,01h,01h,00h,06h,1Fh,0BFh,07h,3Fh,00h,00h,00h,0C1h,0FEh,0Fh, 00h
db 30h dup(0)
db 55h, 0AAh ;55aa標示符號
seg000 ends

end
--> 閱讀更多...

2010年3月3日 星期三

學習u-boot

http://blog.chinaunix.net/u1/34474/showart.php?id=1877493
http://blog.chinaunix.net/u2/75270/showart_1779196.html
http://blog.chinaunix.net/u3/98913/article.html
這兩個連結算是我最近search到最棒的u-boot介紹:
以下內容便是轉載於第二個連結,由於這資料非常寶貴,因此我當然要post一份到自己的blog;以免萬一原文超連結遺失。
u-boot2008.10非nand_leagcy移植mini2440,支持yaffs,附源碼
這篇文章寫於2008.12.28日,主要記錄了我移植u-boot-2008.10的過程,並附上了移植好的patch檔。移植好的u-boot-2008.10適用友善公司的mini2440和陽初公司的yc2410。其他的開發板,可能要根據相應的電路配置做稍許修改。我的移植是使用非nand-leagcy方法的,移植好的u-boot-2008.10功能除了基本功能外,加上了yaffs1映射的寫入功能,加入了從nand flash啟動的功能,改善了一些操作感受,往nand寫入資料時,可以顯示進度。在使用上要注意的是使用nand wirte.yaffs 命令寫入yaffs1映射時,文件長度參數一定要與yaffs1映射的大小完全一致,否則有可能產生假壞塊。我的開發環境是vmware,kubuntu8.04。交叉編譯是用crosstool0.43編譯生成的arm-linux-gcc 4.1.0,libc 2.3.2。
(本人也成功移植了linux2.6.27.9到mini2440開發板上,有需要源碼的,可以和我聯繫。EMAIL:nanjinrat@sohu.com,映射文件在此下載http://blog.chinaunix.net/u2/75270/showart.php?id=1796658 映射的使用方法見我的另一篇文章。http://blog.chinaunix.net/u2/75270/showart.php?id=1836713)一直想自已移植一套u-boot,但因為工作忙,一直都沒有做,最近時間比較多,買了一套友善之臂的mini2440開發板,此板電路與該公司之前的QQ2440基本一至。而該開發板自帶的u-boot的移植的還不很完善,於是下決心自已移植u-boot。要移植就用最新版的u-boot移植,於是決定在u-boot.2008.10版上進行移植,此版是2008年10月的新版,於之前的版本有較大改動,所以版本號也沒有延續以前的編號方式,而改為2008.10。我的移植目的,是要能同時持S3C2440,S3C2410(手中還有一塊陽初公司出的S3C2410開發板),從nand flash啟動u-boot,因為目前大多數應用都是只有nand flash的,所以沒打算從nor flash啟動。支援tftp的使用,也就說要移植網卡的驅動,mini2440和陽初的s3c2410自帶的linux都是使用yaffs檔系統作為根檔系統的,因此,u-boot還要能支援yaffs映射的燒寫。在此我將移植過程記錄下來,以方便大家在移植此版u-boot時參考。為了方便整個移植過程中的調試,我把移植過程分為0~6共7個階段。每個階段完成時,u-boot都是可以正常運行的,因此,你可以根據自已的要求,決定移植工作做到哪一階段。為了同時支持S3C2440和S3C2410,我在移植時,同時加入兩種代碼,使用config_s3c2440,config_s3c2410這樣的巨集定義來決定編譯哪種代碼。如果你不需要同時支援兩個CPU,你可以只加入一種代碼。所有代碼不在此列出,大家可以看我上傳的移植好的u-boot。我在下面給出針對S3C2440移植的詳細說明,S3C2410的移植比較簡單,參考2440的移植即可,不另說明了。不同階段的詳細代碼可以閱讀patch檔。
第0階段:本階段任務,是在u-boot系統中,建立起自已的開發板體系。我先建立mini2440板的體系。方法如下。hugerat是我的網名,你可以根據需要更改。1 打開u-boot主目錄下的makefile,找到smdk2410_config,在其下,仿照它的格式加入如下語句rat2440_config : unconfig @$(MKCONFIG) $(@:_config=) arm arm920t rat2440 hugerat s3c24x0
各項的意思如下:arm: CPU的架構(ARCH)arm920t: CPU的類型(CPU),其對應於cpu/arm920t子目錄。rat2440: 開發板的型號(BOARD),對應於board/hugerat/rat2440目錄。hugerat: 開發者/或經銷商(vender)。s3c24x0: 片上系統(SOC)。
此步是為了加入自已的開發板,非必須也可以在現有的開發板基礎上修改。
2修改CROSS_COMPILE為自已的arm gcc編譯器,我使用的是系統默認的arm-linux-gcc,故不必再修改相應的CROSS_COMPILE項。
3 在/board子目錄中建立自己的開發板rat2440目錄由於我在上一步板子的開發者/或經銷商(vender)中填了 hugerat ,所以開發板rat2440目錄一定要建在/board子目錄中的hugerat
目錄下 ,否則編譯會出錯。然後,將smdk2410目錄下的檔考入此目錄中。並將其中的smdk2410.c改名為rat2440.c 還要記得修改自己的開發板rat2440目錄下的Makefile檔,不然編譯時會出錯:
COBJS := rat2440.o flash.o
4 在include/configs/中建立配置頭檔將smdk2410的相應頭檔複製一份在相同目錄下。並改名為rat2440.h
5 回到u-boot主目錄,make rat2440_config,再make,編譯生成u-boot.bin成功。
建立S3C410板的方法同上,僅將2440改為2410即可。不重複了。
第0階段完成。
第1階段本版u-boot依然沒有提供對S3C2440的支援,因此本階段任務是加入S3C2440相關的代碼,使得u-boot可以在s3c2440上正常工作。但沒有增加任何附加功能。在下列文件中加入標注為/*by hugerat,phase 1---*/的代碼。其中,被注釋掉的代碼為原代碼。/cpu/arm920t/start.s (加入S3C2440的時鐘相關的寄存器定義,加入時鐘初始化代碼,以使S3C2440工作在405MHz)board/hugerat/rat2440/lowlevel_init.s(加入S3C2440記憶體控制寄存器的定義)board/hugerat/rat2440/rat2440.c (修改GPIO,PLL的設置,應注意GPBCON的設置,不要讓蜂鳴器響)include/configs/rat2440.h(設定環境變數)以下檔主要加入CONFIG_S3C2440巨集定義以使得編譯一些S3C2410的代碼,和加入led燈的控制。
以指示u-boot程式進程。/inlcude/s3c24x0.hcpu/arm920t/s3c24x0/interrupts.c(這裏還要加入CONFIG_rat2440和CONFIG_rat2410)/cpu/arm920t/s3c24x0/serial.ccpu/arm920t/s3c24x0/speed.ccpu/arm920t/s3c24x0/usb_ohci.c/cpu/arm920t/s3c24x0/usb.c cpu/arm920t/s3c24x0/i2c.cdrivers/usb/usb_ohci.cdrivers/rtc/s3c24x0_rtc.c/lib_arm/board.c
編譯成功。將u-boot.bin燒入nor-flash即可運行。但是為方便使用,同時,也是方便u-boot的調試因此,我要將此u-boot代碼再做一修改,使其可以在記憶體中運行。這樣,可以用開發板自帶的vivi將其下載到記憶體中,再在記憶體中運行u-boot。要想它在記憶體中運行,方法很簡單,將u-boot/cpu/arm920t中的start.s
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit
#endif
此段代碼中的bl cpu_init_crit注釋掉,即不進行CPU的初始化工作(此工作,當前在板子上運行的vivi已完成,故不能再次進行)
,即改為
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
@bl cpu_init_crit
#endif
修改/board/hugerat/rat2440/config.mk中text_base 值為0x33000000
使用vivi命令load ram 0x33000000 0x17ea8 x(0x17ea8 是載入檔案的size單位byte , x表示xmodem u表示usb)將u-boot.bin裝入記憶體。再用go 0x33000000命令,即可。
至此,第1階段工作完成。
第2階段本階段任務,是給u-boot移植dm9000的網卡驅動。u-boot自帶網卡驅動,所以只要做些設置即可。在下列檔中加入標誌為/*by hugerat,phase 2----*/的代碼./include/configs/rat2440.h (加入dm9000定義,加入ping命令定義)此時,編譯通過,但ping時,報`ethaddr' not set錯誤。研究原代碼,發現#define CONFIG_ETHADDR 08:00:3e:26:0a:5b被注釋掉,恢復,並改變原來默認的IP位址。重新編譯後,可以ping通網路了。(也可以在u-boot啟動後,修改相關參數,但因為現階段還沒有支持nandflash,參數無法保存,故在此改變較為方便)又發現,ping是能ping通了,但報could not establish link(不能建立鏈結)的錯。不影響使用。參考網上的資料,發現這是因為在網卡驅動中,/home/lijin/u-boot-rat/drivers/net/dm9000.c,有一段程式試圖連接網卡的MII介面,而實際上MII介面並未使用,所以有十秒的等待時間,且報錯,將此段程式注釋掉即可。到此,本階段任務完成。
第3階段本階段任務,移植nand-flash驅動,注意此階段工作僅是讓u-boot可以操作讀寫nand flash。還不能讓它從nand flash啟動。
首先,要說明一下CFG_NAND_LEGACY的使用。在u-boot的/drivers/mtd/下有兩個目錄,分別是nand和nand_legacy。在nand目錄下的是nand的初始化函數和nand的操作讀寫函數,是使用linux的mtd構架的。此目錄下的檔,只有在定義了CFG_CMD_NAND宏和沒有定義CFG_NAND_LEGACY巨集的情況下才會被編譯。在nand_leagcy目錄下的檔也是是實現nand相關操作命令,如read,write等命令的功能,但不是使用linux的mtd構架。此目錄下的檔,只有在定義了CFG_CMD_NAND和定義了CFG_NAND_LEGACY巨集的情況下才會定義。此目錄下的檔u-boot組織已不推薦使用。事實上,此版中,S3C2410構架已不支持對nand_leagcy,因此,我在移植中,是用的不定義CFG_NAND_LEGACY的方式,即非nand_leagcy方式。
在/include/configs/rat2440.h中加標注為by hugerat,phase3的代碼段,這段代碼是讓u-boot啟用其自帶nand flash驅動,並設置相應的nand flash參數。此時,試編譯一下,通過。要說明的是,此版的u-boot已自帶board_nand_init(),此函數在/cpu/arm920t/s3c24x0/nand.c中實現。並且此版已不支援定義CFG_NAND_LEGACY,如定義此宏,則編譯是會報 #error "U-Boot legacy NAND support not available
for S3C2410"的錯誤。故此版已不能使用網上流傳非nand_leagcy方式的自加nand flash初始化函數的方法,只能用其自帶的初始化函數下載到記憶體中運行,報None nand devices!!!,這是當然的。因為S3C2410和S3C2440在FLASH控制器上,差別較大,必須改寫代碼。
改寫主要在/cpu/arm920t/s3c24x0/nand.c中進行。(此檔內是與晶片緊密相關的代碼),一是board_nand_init函數,一是s3c2410_hwcontrol,參照2440的手冊,改寫相關代碼。還要在此檔的開始部分加入S3C2440 nand flash控制器相關寄存器的定義。改寫的代碼標注同上。改寫完畢後,u-boot可以識別出nand flash晶片是64MB的,但還不能識別是什麼晶片。
在/driver/mtd/nand/nand_base.c中的nand_get_flash_type函數結尾,修改MTDDEBUG語句,改為printf,再編譯,可以正常顯示芯片了。但nand write功能不正常,即沒報錯,實際上也沒有寫進去。
探討原因後發現,原來是u-boot自帶的nand-flash驅動(不定義nand_leagcy),是基於mtd驅動的。在默認情況下,不進行寫入正確與否的校驗。要定義CONFIG_MTD_NAND_VERIFY_WRITE宏才能進行寫入校驗。關於ECC校驗,mtd驅動默認是用sotf_ecc的。加入宏定義後,u-boot報write error了。在此我卡了很久,最後才發現是u-boot.2008.10自帶的S3C2410的s3c2410_hwcontrol函數有錯。在此函數中,把chip->IO_ADDR_W值改寫了,導致在寫資料時出現錯誤。將此錯誤修正後,nand write正常了。修正方法是使用一總體變數替代chip->IO_ADDR_W。
接下來,在rat2440.h中加入#define CONFIG_ENV_IS_IN_NAND 1注掉原來的#define CONFIG_ENV_IS_IN_FLASH 1,加入
#define CONFIG_ENV_OFFSET 0x30000 注掉原來的#define CFG_ENV_OFFSET 0x30000。編譯。saveenv功能也正常了。至此,nand-flash驅動移植完成。
測試,nand write 0x30000000 0x40000 0x40000時,成功。用nand read也成功讀出。此處要說明的,如果用vivi燒寫資訊到nand
中,再用u-boot讀取,會報錯,應該是ECC校驗不是由同一軟體產生所至。此階段完成。
第4階段本階段任務是將u-boot改寫為從nand-flash啟動。首先,將/cpu/arm920t/start.s中第一階段中為了從記憶體中啟動而遮罩掉的語句恢復。加入標注為/*by hugerat,phase 4----*/
語句,為了相容S3C2410,同時也加入S3C2410 nand boot 相關代碼。
同時,在/board/hugerat/rat2440/lower_init.s中,根據開發板電路,對記憶體相關的幾個寄存器定義進行調整。整個啟動代碼參考了vivi的代碼。因此,將vivi的nand_read.c(支持S3C2440
的vivi,此檔中的讀nand函數是直接操作nand flash的,與u-boot自帶,適合用於啟動時用。)考入/board/hugerat/rat2440目錄下。並修改目錄下的makefile,使nand_read.c被編譯。編譯,成功,將u-boot燒入nand flash,能成功從nand啟動了。
第5階段本階段任務是實現u-boot通過nfs引導友善提供的linux2.6.13的內核。友善的linux並沒有提供make uImage命令,需要添加。將u-boot的tools目錄下的mkimage拷到開發機linux系統下的/usr/bin目錄下
。在linux的/arch/arm/makefile中和/arch/arm/boot/makefile中,可以找到zImage的項目,仿照它添加uImage項目。
這個問題解決後,就是要用u-boot引導linux內核了,我用的是揚創提供的linux2.6.13的內核,揚創提供的內核,loadaddress和entry都是30008000,我用u-boot1.2中的bootcmd參數,u-boot1.3.1 bootm卻不能引導,報bad magic number,看了bootm的相關資料,得知,如果我們沒用mkimage對內核進行處理的話,那直接把內核下載到0x30008000再運行就行,內核會自解壓運行(不過內核運行需要一個tag來傳遞參數,而這個tag建議是由bootloader提供的,在u-boot下默認是由bootm命令建立的)。 2)如果使用mkimage生成內核鏡像檔的話,會在內核的前頭加上了64byte的資訊,供建立tag之用。bootm命令會首先判斷bootm xxxx 這個指定的位址xxxx是否與-a指定的載入位址相同。(1)如果不同的話會從這個位址開始提取出這個64byte的頭部,對其進行分析,然後把去掉頭部的內核複製到-a指定的load地址中去運行之(2)如果相同的話那就讓其原封不同的放在那,但-e指定的入口地址會推後64byte,以跳過這64byte的頭部。
Bootm在沒有參數時,是採用rat2440.h中的#define CFG_LOAD_ADDR 的位址的,而我用bootm就是沒有使用參數,所以出錯了。正確的做法應該是用nand read命令將內核從nand flash中讀到記憶體的某一位址中(注意不要與其他已分配的記憶體衝突),然後再用bootm 加位址參數,即可引導,也可以在上述的檔中,將CFG_LOAD_ADDR的位址定義為此位址,再用bootm就可以了.我設定bootcmd環境變數為tftp 0x31000000 uImage; bootm 0x31000000,注意位址不能為0x30008000,否則報錯.
做完這此,內核可以引導了,但卻停在starting kernel不動了,好在我以前做過vivi+linux2.6.22的移植,知道此問題多半是由於mach_type不同而造成的。在u-boot中,此mach_type是由rat2440.c中的這段代碼定義的
#if defined(CONFIG_S3C2440)
/* arch number of S3C2440 -Board */
//gd->bd->bi_arch_number = 5244 ; //改為和內核的MACH_TYPE一至
gd->bd->bi_arch_number = MACH_TYPE_MINI2440 ;
#endif
你可以直接在這裏改數位,也可以在include/asm-arm/mach_types.h的文件中,改MACH_TYPE_S3C2440的數值。將數值改為和內核的
mach_type一至。至於內核的mach_type可以在內核linux源代碼下的arch/arm/tools中的mach_types檔查看到。
此時,又發現內核不能通過nfs引導。查資料後得知是因為u-boot沒有傳遞參數給內核。原來是rat2440.h中少定義了幾個宏。補上
。#define CONFIG_CMDLINE_TAG 1 /* enable passing of ATAGs */
#define CONFIG_SETUP_MEMORY_TAGS 1
#define CONFIG_INITRD_TAG 1
至此,u-boot已能正常引導linux啟動了。第5階段完成。
第6階段本階段任務,是給u-boot加入燒寫yaffs映射檔的功能。網上有很多文章已經有介紹如何加入此功能,方法也不複雜。但我移植的u-boot.2008.10版,在nand flash的操作上與之前的版本
有了很大的區別。這些文章介紹的方法已不能在此版中應用。於是我只好自已根據u-boot的源代碼,yaffs映射檔的格式,自已編修改了u-boot.2008.10的代碼,讓它可以實現yaffs的燒寫,因為我用的開發板的nand flash是k9f1208,只能使用yaffs1,所以我的修改代碼暫時只能支援yaffs1格式的映射。(要支持yaffs2也不難,但因為無法驗證,所以就沒有做嘗試。)修改方法如下:在/common/com_nand.c中do_nand函數中,加入三段/*by hugerat,phase6---*/標注的代碼,實現對nand write.yaffs命令的支援。
此代碼中,要用到mtd_info結構中的兩個變數,這兩個變數本來是沒有的,所以要在include/linux/mtd/mtd.h的mtd_info結構體定
義中加入。在/drivers/mtd/nand/nand_util.c的nand_write_skip_bad函數中,兩段程式,一段是為了計算正常資料的長度,一段是為了在寫入一段資料後,資料指標能正常跳到下一段資料。在/drivers/mtd/nand/nand_base.c的nand_write函數中,加入一段把正常資料與oob資料分離的代碼,再加入頁寫時的模式設置為MTD_OOB_RAW,使頁寫時,不進行ECC的校驗和加入。(ECC的校驗在yaffs的oob資料中已自帶了,不能重寫。)此模式下,寫入正常資料後,會把oob資料緩存的資料寫入nand的oob區。至此,u-boot.2008.10的移植工作全部完成。
最後說明一下,關於陽初2410的移植,因為上面的代碼中已加入S3C2410相關代碼,移植時,只需參考S3C2440,新建一個開發板體系,新建時,將rat2440目錄下的檔拷貝到rat2410下,將重命名S3C2440.c為S3C2410.c,修改makefile,加上s3c2410.o 去掉s3c2440.o,拷貝rat2440.h為rat2410.h,將其中的CONFIG_S3C2440注掉,加上CONFIG_S3C2410。在u-boot總目錄下的makefile中rat2440_config下添加rat2410_config : unconfig @$(MKCONFIG) $(@:_config=) arm arm920t rat2410 hugerat s3c24x0因為陽初的板用的是cs8900,因此要在rat2410.h中注掉DM9000的定義,添加CS8900的。再make rat2410_config,再make,即可。
文章最後,為方便網友,我把patch檔附上。patch文件使用方法是在linux下建立u-boot-rat.patch檔,將patch檔的內容拷入u-boot-rat.patch文件中。下載u-boot-2008.10.tar.bz2,在這裏下載ftp://ftp.denx.de/pub/u-boot/,用tar -xvf u-bot-2008.19.tar.bz2解壓到u-boot-2008.10目錄,把u-boot-rat.patch檔拷貝到此目錄所在同級目錄下。執行patch -p0 小於符號 u-boot-rat.patch。完成後,進u-boot-2008.10目錄,執行make rat2440_config,再make即可生成u-boot.bin,如是2410,則執行make rat2410_config即可,我在mini2440,陽初的2410上試過,功能正常。其他開發板的也可一試。 附patch文件: 可能發貼字數有限制,下面的補丁檔不全。可以在此下載全部補丁。 文件:u-boot-rat.rar 大小:20KB 下載
--> 閱讀更多...

2010年3月1日 星期一

Fast A20 和92H,及8042到底是什麼關係



關於A20 Gate
本來想直接寫一篇關於保護模式的文章,因為有一位讀者不斷地問我這個問題,隨著問題的深入,在評論上回答這個問題實在是太困難了,動起筆來,發現涉及的事情太多,免不了又是長篇大論惹人煩,而且要寫很長時間,不知道我能不能把它寫完,所以乾脆把一些問題分離出來寫,或許還可以堅持寫出來。
在許多PC的CMOS設置裏,都有一項叫做“A20 Enable“的設置,不知道大家是否就此設置困惑過,這個A20是什麼呢?
說A20則不得不說PC機的記憶體,我儘量用簡短的語言說明白PC機記憶體的一些獨有的術語,在我剛接觸PC機的時候(1985年前後),如果你的機器有一塊10M或者20M的硬碟,那已經是一台不錯的機器,如果你的機器有256K的記憶體,那絕對是高配置,那時候還沒有3.5"的軟碟,都是使用5"的容量僅360K的軟碟,所以那個時候,設計IBM PC的IBM公司非常自信地認為,1M記憶體是一個根本達不到的天文數字,我想由於這種思想的作怪(我猜的),IBM非常愚蠢地把PC機1M存儲空間的最上面的384K用作了ROM和系統設備,這種設計給現在的PC機的記憶體結構埋下了麻煩的伏筆,如果當初IBM把PC機的1M存儲空間的最下面的384K用作ROM和系統設備,可能現在就會少好多麻煩,這個後面說。
寫到這裏的時候,我想起了許多事,大家是否知道我國的第一台電子電腦是什麼型號?是什麼樣子?是1958年生產的103機,後來又有了全電晶體的電子電腦,很榮幸的是,在我工作過的單位裏,曾經有過這麼一台全電晶體的電子電腦,就是那種使用穿孔紙帶輸入電腦程式,占地好幾百平方米,一旦開動需要幾十人進行維護的傢伙,我到這個單位的時候,這台機器早已不再運轉的,80年代的時候,我有幸參加了這台機器的拆解工作,給我印象最深的是,我終於看到了當時在教科書裏說的“磁鼓”(估計現在的教科書裏也沒有了),一種看上去像篩子一樣的東西,那就是當年的記憶體,用於在紙帶上打孔進行程式輸入的穿孔機我用過,樣子很像老式的英文打字機。還有那種8英寸的軟碟,估計現在也很少有人見過了。
好了言歸正傳,大家都知道,8088/8086只有20位位址線,按理它的定址空間是2^20,應該是1024KB,但PC機的定址結構是segment:offset,segment和offset都是16位的寄存器,最大值是0ffffh,換算成物理位址的計算方法是把segment左移4位,再加上offset,所以segment:offset所能表達的定址空間最大應為0ffff0h + 0ffffh = 10ffefh(前面的0ffffh是segment=0ffffh並向左移動4位的結果,後面的0ffffh是可能的最大offset),這個計算出的10ffefh是多大呢?大約是1088KB,就是說,segment:offset的位址表達能力,超過了20位元位址線的物理定址能力,你說這是不是有點麻煩。在早先,由於所有的機器都沒有那麼大的記憶體,加上位址線只有20位,所以當你用segment:offset的方式企圖定址100000h這個位址時,由於沒有實際的第21位位址線,你實際定址的記憶體是00000h的位置,如果你企圖定址100001h這個位址時,你實際得到的內容是位址00001h上的內容,所以這個事對實際使用幾乎沒有任何影響,但是後來就不行了,出現了80286,地址線達到了24位,使segment:offset定址100000h--10ffefh這將近64K的記憶體成為可能,為了保持向下相容,於是出現了A20 Gate,這是後話,我們後面再細說。
我們可能經常聽到一些只有在PC機上才有的一些關於記憶體的專有名詞,包括:常規記憶體(Conventional Memory)、上位記憶體區(Upper Memory Area)、高端記憶體區(High Memory Area)和擴展記憶體(Extended Memory),我儘量把這幾個東東說明白,這需要上面這張著名的圖。

這張圖很清楚地說明了問題,大家都知道,DOS下的“常規記憶體”只有640K,這640K就是從0--A0000H這段位址空間;所謂“上位記憶體區”,指的就是20位位址線所能定址到的1M位址空間的上面384K空間,就是從A0001H--100000H這段位址空間,也就是我們說的用於ROM和系統設備的位址區域,這384K空間和常規記憶體的640K空間加起來就是20位位址線所能定址的完整空間1024KB;由於80286和80386的出現使PC機的位址線從20位變成24位又變成32位,定址能力極大地增加,1M以上的記憶體定址空間,我們統稱為“擴展記憶體”;這裏面絕大部分記憶體區域只能在保護模式下才能定址到,但有一部分既可以在保護模式下,也可以在實模式下定址,這就是我們前面提到過的位址100000h--10ffefh之間的這塊記憶體,為了表明其特殊性,我們把這塊有趣的記憶體區叫做“高端記憶體”。
前面我們提過由於IBM的愚蠢設計給PC機的記憶體結構埋下了麻煩的伏筆,現在我們來說說這個麻煩。我們都見過PC機上的記憶體條,但是由於上位記憶體區的存在,這個記憶體條上的位址居然不能連續,就是說,這個記憶體條上要有0--A0000H的位址空間,還要有100000h--最大記憶體容量的位址空間,中間的384K位址空間必須留出來給ROM用,在現如今一個晶片就好幾兆的情況下,你說這個記憶體條應該怎麼做,當然我相信一定是可以做出來的,但肯定很麻煩,如果當初IBM把這個“上位記憶體區”放在位址低端,就是0--6000h這一部分,豈不是這個麻煩就沒有了?!
但是,實際的記憶體條上位址都是連續的,並沒有人把這段位址空間留出來給ROM使用,原因很簡單,採用技術手段把這段位址空間空出來,比浪費這384K記憶體的成本還要高,所以在這個位址區域就出現了很奇怪的現象,ROM和RAM的地址重疊。 實際上,往往ROM並不能完全覆蓋整個384K區域,這樣就會有一些位址沒有被ROM佔用,那麼這部分位址上的RAM仍然是可以使用的。實際上,和ROM重疊的這384K RAM一般也不會被浪費,說到這裏,不得不說所謂的ROM Shadowing了,RAM和ROM的性能是有很大差異的,RAM的存取速度要遠遠大於ROM,而且RAM可以32位存取,ROM通常只能16位,所以目前的PC機對這塊RAM和ROM重疊的區域的處理採用一種ROM Shadowing的技術方式,當機器加電後,先讓ROM有效,RAM無效,然後讀出ROM內容,再讓ROM無效,RAM有效,把讀出的ROM內容放到相同位址的RAM中,並把相應位置的RAM設定為唯讀,這樣就把ROM搬到了RAM中,位址完全一樣,只是性能比使用ROM要高些,這塊RAM就好像ROM的Shadow一樣。
回到我們的主題A20 Gate,出現80286以後,為了保持和8086的相容,PC機在設計上在第21條位址線(也就是A20)上做了一個開關,當這個開關打開時,這條位址線和其他位址線一樣可以使用,當這個開關關閉時,第21條位址線(A20)恒為0,這個開關就叫做A20 Gate,很顯然,在實模式下要訪問高端記憶體區,這個開關必須打開,在保護模式下,由於使用32位位址線,如果A20恒等於0,那麼系統只能訪問奇數兆的記憶體,即只能訪問0--1M、2-3M、4-5M......,這顯然是不行的,所以在保護模式下,這個開關也必須打開。
下面我們來看一下PC機是怎麼實現A20 Gate的。
早期的PC機,控制鍵盤有一個單獨的單片機8042,現如今這個晶片已經給集成到了其他大片子中,但其功能和使用方法還是一樣,當PC機剛剛出現A20 Gate的時候,估計實在找不到控制它的地方了,同時為這點小事也不值得增加晶片,於是工程師使用這個8042鍵盤控制器來控制A20 Gate,但A20 Gate真的和鍵盤一點關係也沒有,我們先從軟體的角度簡單介紹一下8042這個晶片。
8042有4個寄存器
· 1個8-bit長的Input buffer;Write-Only;
· 1個8-bit長的Output buffer; Read-Only;
· 1個8-bit長的Status Register;Read-Only;
· 1個8-bit長的Control Register;Read/Write。
有兩個埠位址:60h和64h。
· 讀60h埠,讀output buffer
· 寫60h埠,寫input buffer
· 讀64h埠,讀Status Register
對Control Register的操作相對要複雜一些,首先要向64h埠寫一個命令(20h為讀命令,60h為寫命令),然後根據命令從60h埠讀出Control Register的資料或者向60h埠寫入Control Register的資料(64h埠還可以接受許多其他的命令)。
先來看看Status Register的定義,我們後面要用bit 0和bit 1:
bit meaning ----------------------------------------------------------------------- 0 output register (60h) 中有數據

1 input register (60h/64h) 有數據

2 系統標誌(上電重定後被置為0)

3 data in input register is command (1) or data (0)

4 1=keyboard enabled, 0=keyboard disabled (via switch)

5 1=transmit timeout (data transmit not complete)

6 1=receive timeout (data transmit not complete)

7 1=even parity rec'd, 0=odd parity rec'd (should be odd)
除了這些資源外,8042還有3個內部埠:Input Port、Outport Port和Test Port,這三個埠的操作都是通過向64h發送命令,然後在60h進行讀寫的方式完成,其中本文要操作的A20 Gate被定義在Output Port的bit 1上,所以我們有必要對Outport Port的操作及埠定義做一個說明。
· 讀Output Port向64h發送0d0h命令,然後從60h讀取Output Port的內容
· 寫Output Port向64h發送0d1h命令,然後向60h寫入Output Port的資料
另外我們還應該介紹兩個命令:
· 禁止鍵盤操作命令向64h發送0adh
· 打開鍵盤操作命令向64h發送0aeh
有了這些命令和知識,我們可以考慮操作A20 Gate了,有關8042晶片更詳細的資料,請參考該晶片的Data Sheet。
如何打開和關閉A20 Gate。
理論上講,我們只要操作8042晶片的輸出埠(64h)的bit 1,就可以控制A20 Gate,但實際上,當你準備向8042的輸入緩衝區裏寫資料時,可能裏面還有其他資料沒有處理,所以,我們要首先禁止鍵盤操作,同時等待資料緩衝區中沒有資料以後,才能真正地去操作8042打開或者關閉A20 Gate。打開A20 Gate的具體步驟大致如下:
1.關閉中斷; 2.等待8042 Input buffer為空; 3.發送禁止鍵盤操作命令; 4.等待8042 Input buffer為空; 5.發送讀取8042 Output Port命令; 6.等待8042 Output buffer有數據; 7.讀取8042 Output buffer,並保存得到的位元組; 8.等待8042 Input buffer為空; 9.發送Write 8042 Output Port命令到8042 Input buffer; 10.等待8042 Input buffer為空; 11.將從8042 Output Port得到的位元組的第2位置1(或清0),然後寫入8042 Input buffer; 12.等待,直到8042 Input buffer為空為止; 13.發送允許鍵盤操作命令到8042 Input buffer; 14. 打開中斷。
下面是完成打開A20 Gate的代碼:
A20Enable:

cli ;1.關閉中斷

call WaitInbufEmpty ;2.等待8042 Input buffer為空;

mov al, 0adh

mov dx, 64h

out dx, al ;3.發送禁止鍵盤操作命令

call WaitInbufEmpty ;4.等待8042 Input buffer為空;

mov al, 0d0h

mov dx, 64h

out dx, al ;5.發送讀取8042 Output Port命令;

call WaitOutbufFull ;6.等待8042 Output buffer有數據;

mov dx, 60h

in al, dx ;7.讀取8042 Output buffer

push ax ;保存讀取的資料

call WaitInbufEmpty ;8.等待8042 Input buffer為空;

mov al, 0d1h

mov dx, 64h

out dx, al ;9.發送寫 8042 Output Port命令

call WaitInbufEmpty ;10.等待8042 Input buffer為空

pop ax

or al, 02h ;11.將從8042 Output Port得到的位元組的bit 1置1

mov dx, 60h

out dx, al ;寫入Output Port

call WaitInbufEmpty ;12.等待8042 Input buffer為空

mov al, 0aeh

mov dx, 64h

out dx, al ;13.發送允許鍵盤操作命令

sti ;開中斷

ret

WaitInbufEmpty:

mov dx, 64h

in al, dx ;讀取Status Register

test al, 02h

jnz WaitInbufEmpty

ret

WaitOutbufFull:

mov dx, 64h

in al, dx

test al, 01 ;讀取Status Register

jz WaitOutbufFull

ret
後來,由於感覺使用8042控制A20運行太慢了(確實,那麼長的代碼,中間還要若干次的wait),所以後來又出現了所謂的Fast A20,實際上,現在的大多數機器都是Fast A20,Fast A20使用92h埠控制A20,同時BIOS裏提供了一個軟中斷來控制A20:
入口:ah=24h al=0 關閉A20 1 打開A20 2 讀取A20狀態 int 15h
返回:如果BIOS支援此功能,CF=0,否則CF=1 CF=0時,AX返回當前A20狀態,1=打開,0=關閉
像8042中的Output Port中的定義一樣,92h埠的bit 1控制著A20,為1時打開,為0時關閉,從92h中讀一個byte可以看a20的當前狀態,所以對92h的操作如下:
· 讀A20狀態mov dx, 92hin al, dx如果al的bit 1為1表示a20打開,否則為0
· 打開A20mov dx, 92hmov al, 02out dx, al
· 關閉A20mov dx, 92hmov al, 0out dx, al
特別要注意的是,大家從這篇文章的文字中可能也能感覺到,A20 Gate的設計本身就讓人感覺很彆扭,不是那麼流暢,所以和A20有關的事情就難免也會有相同的感覺,很奇怪的是,上面介紹的三種方法並不是在每台機器上都適用,所以如果你要做一個商業軟體其中要操作A20,那一定要三種方式聯合使用才比較穩妥,否則會有意想不到的結果,LINUX公開的啟動代碼中就是這麼做的
在DOS下有時我們會在config.sys中寫上一句:dos=high,這句就會把駐留的DOS放到高端記憶體區域去,怎麼放的呢?關鍵點就是打開A20,然後把DOS從常規記憶體搬到100000h起始的區域去,並把在常規記憶體中佔用的記憶體釋放掉,當然說起來容易,實際做的時候還有很多細節要處理。
說到DOS=HIGH,就不得不提醒大家另一件事,如果在config.sys中有dos=high這一句,那麼恐怕92h的方法和BIOS的方法都會不完全靈驗(至於操作8042的方法靈不靈我沒有試),這是DOS做了手腳,因為DOS被放到了高端記憶體中,為了保證DOS能正常運行,它不允許你把A20給關掉,遇到這種情況不要驚慌,不是我寫得不對,確實是DOS太狡猾了。
還有最後一點要特別注意,92h的bit 0是給機器發重定信號的(8042 Output Port的bit 0也是),所以在向92h寫資料時,千萬不要讓bit 0為1,否則機器會重新啟動,如果你的應用程式需要重新啟動機器,這也是方法之一,比jmp 0ffffh:0來的還要乾脆。
在其他我們介紹保護模式的文章中,我們會用到上面提到的打開A20的方法,屆時可能就不會做更多的解釋了。
另外,涉及操作A20的資料其實很少,有些資料我手裏也很缺乏,比如92h除bit 0和bit 1以外的定義是什麼我至今也不知道,包括本文中的一些內容也是摸索所得,並沒有資料予以佐證,所以萬一有不對的地方,不要見怪,並懇請指出。
--> 閱讀更多...

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月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
--> 閱讀更多...

2009年12月14日 星期一

●memtest86+教學 Part16

關於SMP在Memtest86+ V4.00與Memtest86 V3.5 之比較可以發現main.c中多了void initialise_cpus(void),因此先將Memtest86 V3.5 轉成exe檔來執行看看多加入SMP功能後測試時間是否減少.得知結果差異不大,因為其實作多核只是以輪流交替執行權來執行程式碼。我想未來應該會有改進吧,到時後就可以享受多核心CPU降低測試時間的好處了。
注意一點Memtest86 V3.5必須使用gcc 4-2-0以下的版本compile,但Memtest86+ V4.00則使用gcc 4-2-x以上版本。
--> 閱讀更多...