2009年3月31日 星期二

●DJGPP和保護模式

本文轉貼於http://hengch.blog.163.com/
DOS不能工作在保護模式
讓CPU工作在保護模式很容易,但CPU工作在保護模式時你無法調用DOS和BIOS服務,為什麼呢?因為DOS和BIOS的代碼是按照在真實模式下運行的方式寫的,不符合保護模式程式的規範,比如,在真實模式下,DOS下的程式碼可以把任意數值放到段寄存器(CS、DS、ES、SS)中,只要不超過64K就可以,但在保護模式下,段寄存器只能放一個已經存在的selector的值,任何其它的值都將引起一個“General Protection Fault”錯誤異常。
所以,如果程式讓CPU進入保護模式,並且調用DOS服務,比如列印一行資訊,將馬上使系統崩潰,如果你不明白這一點,恐怕連一個最簡單的“Hello World”程式都寫不出來。
更糟糕的是,雖然應用程式不能調用DOS和BIOS服務,但DOS和BIOS卻必須要運行,比如時鐘晶片產生的硬體中斷,每秒18.2次的時鐘中斷等,時鐘中斷是BIOS的一部分,工作在真實模式。
所以,哪怕程式不調用任何真實模式代碼,一些非同步的系統事件仍然會發生,機器仍然會很快崩潰,所以,要在DOS下進入保護模式,必須首先解決DOS/BIOS和保護模式之間的這種衝突。
DOS Extender允許DOS和保護模式共存
如果你不想寫一個保護模式下的作業系統來完全取代DOS和BIOS,解決衝突的辦法是在你的應用程式和DOS/BIOS之間加入一個軟體層,這個軟體層可以視情況讓CPU在真實模式和保護模式之間切換,這個軟體層叫做DOS Extender。
在DOS Extender下,當保護模式下的程式要調用真實模式下的服務時,Extender給這個調用設置一個陷阱,把CPU切換到真實模式,再重新發出 這個調用,等待其完成調用,然戶再切換回保護模式,並返回到那個調用真實模式代碼的應用程式中,像時鐘中斷、鍵盤這樣的硬體中斷,也會被Extender設 置一個陷阱,並產生保護模式到真實模式的切換和返回。
你可能會想,這種方式會使應用程式運行變的很慢,然而在實際應用中,大多數程式並不會頻繁地調用OS的服務,即使調用很多,由於大多數的OS服務都是存取外部設備,比如硬碟,而這些設備的速度比起CPU而言是非常慢的,所以很少有人注意到模式切換上的系統開銷。
DJGPP v1.X和go32 Extender
在DJGPP v1.x中使用的go32程式,就是這樣一個DOS Extender,每個程式啟動時都會自動地裝載go32,go32除了完成DOS Extender的任務外,還接管下面一些與DJGPP相關的任務。
• 裝載應用程式,並為運行做好準備
由於DJGPP的執行檔使用COFF格式,DOS看不懂這種格式,go32負責讀取 COFF頭並初始化代碼、資料和其它檔頭中的分段
• 提供UNIX形式的命令列擴展
• 保護模式下浮點運算模擬
• 圖形支援
go32中有一些特殊的代碼,使其能夠適應各種各樣的進入保護模式的方法並管理擴展記憶體,所以他可以工作在任何DOS配置下,但這也使其產生一些不能忽視的缺陷,就是Extender必須被裝入常設記憶體,並且每個程式實例大約要佔用130KB的記憶體,大多數情況下DOS啟動以後都會有500-600K剩餘的常設記憶體,這意味著一個DJGPP程式只能有3-4級的嵌套。這是一個很嚴重的限制,DJGPP v2.x已經解決了這個問題。
DJGPP v2.X與DPMI服務
DJGPP v2.x放棄了Extender,取而代之的是需要一個已經運行的DPMI服務,DPMI:DOS Protected-Mode Interface的縮寫,是一個特殊的API,它允許保護模式的應用程式在DOS的上層運行,他定義了 一些函數,使保護模式下的程式(稱為DPMI客戶)可以做一些諸如:進入保護模式、分配記憶體和段描述符、調用真實模式的服務、連接中斷等等,許多使用 Intel CPU的作業系統都有DPMI服務,包括windows的所有版本,OS/2,以及LINUX DOS模擬器都是著名的例子,還有一些持有專 利的DOS下的DPMI伺服器,通常和DOS的記憶體管理器捆綁在一起,比如QEMM和386MAX,FreeDOS 也包含有一個DPMI伺服器作為缺省設置的一部分,對於那些沒有DPMI伺服器的系統,DJGPP v2.x提供一個免費的DPMI伺服器,叫做 CWSDPMI,CWSDPMI很多地方使用了go32的代碼,DJGPP啟動代碼檢查DPMI服務,如果沒有,將自動搜索並載入 cwsdpmi.exe----CWSDPMI伺服器。
DPMI伺服器(又稱為DPMI HOST)可以解決運行在DOS上層的保護模式程式的大部分問題,餘下的那些在1.x版本中由go32完成的函數,在v2.x中被DJGPP的啟動代碼接管並放在低級函式程式庫中,下面簡短地說一下DJGPP啟動代碼的兩個特點。
DJGPP v2.x的啟動代碼
DJGPP v2.x啟動代碼包括兩部分:短小的裝載程式和庫啟動模組,前者是由組合語言寫成,是一段有特殊作用的組合語言程式,叫做djasm,它是一段 16-bit的DOS可執行程式,這個短小的裝載程式會被連結到每一個DJGPP程式中,是唯一一部分可以被DOS識別的部分,其餘部分----COFF 可執行文檔----在DOS看來只是一些奇怪的資料。
第二部分是一個庫模組,它包括許多模組,有些用C寫成,有些用彙編寫成,當裝載程式把程式調入並初始化完成後,這裡就是COFF格式程式的入口點。
裝載程式完成以下工作:
• 為傳輸緩衝區申請記憶體
這個緩衝區用於在DOS服務和程式之間傳送資料。
• 檢查是否DPMI服務已經運行
以下兩種情況說明DPMI已經啟動
(1)有一個常駐記憶體的DPMI伺服器,比如windows中內置的DPMI伺服器
(2)當前程式是一個嵌套的DJGPP程式,他的父程式已經啟動了CWSDPMI
如果DPMI服務還沒有啟動,則裝入CWSDPMI。首先在目前的目錄下搜索cwsdpmi.Exe, 然後到環境變數PATH指定的目錄下去查找。
• 把COFF可執行部分的檔頭裝入記憶體
需要知道要為DJGPP程式申請多少記憶體
• 調用DPMI host提供的入口點,把CPU切換到保護模式
注意:裝入程式的其餘部分運行在保護模式下
• 為程式碼和資料申請記憶體空間
通過DPMI的功能調用可以為代碼和資料申請段描述符和記憶體空間,並設置基底位址、 界限和許可權。
• 把COFF格式的可執行部分調入記憶體
通過DPMI服務調用DOS(主要指檔操作)服務,把代碼、資料和BSS節讀入上 面申請的記憶體中,DPMI服務允許你從保護模式下調用真實模式下的服務。
• 跳轉到COFF鏡像的入口點執行
這個入口點在上面提到過的庫啟動模組中。
下面是庫啟動代碼部分完成的工作
• 生成一個不受約束的空頁
這將產生一個NULL Pointer dereference的錯誤,並將觸發一個異常處理,程式 會收到SIGSEGV信號,但這個功能並非基本DPMI 0.9規範中的一部分,所以 windows和其它許多有專利的DPMI伺服器並不支持這個功能,但CWSDPMI支援這 一功能。
• 改變申請記憶體的資料段的大小
這個聽起來簡單,但由於DPMI記憶體調用的特殊性,實際上是非常複雜的,例如: 它需要把一段真實模式下的16-bit代碼調入常設記憶體的緩衝區並運行。
• 設置程式的堆疊
DJGPP程式堆疊的缺省大小是512KB,但應用程式或改變設置都可以改變堆疊大小
• 為存取常設記憶體申請selector
與DOS/BIOS函數之間傳遞資料,或者像視頻界面上的顯示緩衝區那種使用記憶體 映射的設備,許多DOS程式需要存取常設記憶體,但由於在缺省情況下,常設記憶體並 沒有映射在程式的資料段中,為了存取常設記憶體,使用了一個特殊的selector---- _dos_ds。
• 初始化信號管理
需要連結一些硬體中斷,例如:按下CTRL-時產生的SIGINT信號。還有時鐘中 斷產生的SIGPROF信號等。
• 拷貝程式的環境變數到environ[]陣列中
• 讀出定義了DJGPP附加環境變數的檔
• 獲得並解釋命令列參數
• 如果需要,設置x87 FPU並載入浮點運算模擬器
• 調用靜態構造函數
• 調用用用程式的主函數
--> 閱讀更多...

博客心情

博客大陸人稱呼部落格為博客,不知為何寫博客會上癮,可能是我這ㄍ宅男實在太無趣ㄌ,所以才透過文字抒發心情,然而每次寫完一篇就會覺得心情好過些,就好像每天早上ㄉ那杯咖啡,我目前的工作整天就是在跟記憶體測試軟體打交道,雖說不上心得滿滿,但從剛開始的牙牙學語,到現在一年半載,至少該懂的應該也有七八成,Debug也成為我的嗜好之一,一開始抱持著以前寫ap都搞不懂(應該說領域不同,所以沒機會接觸更深入)的,這回有機會一定要以頃城之力來回報自己內心的不踏實。雖然我很想跳出這個dram產業去接觸更多‧‧‧,但其實不那麼迫切,因為我覺的自己還有成長的空間,學習x86這一塊,讓我感觸良多,畢竟這是整個IT產業的最大宗,然而我相信搞好這一塊,要跳到其它嵌入式系統應該熟悉度會加快。雖說技術一日千里,但只要你夠耐心及細心,我想你也可以日進萬里(內心ㄉ成長)。
--> 閱讀更多...

2009年3月30日 星期一

●分解BIOS

注意:本網站討論的部分內容不詳,還沒瞭解透,定義為:不清昕,可能有錯誤的。

這裡主要以Intel平臺的BIOS檔討論,輔助參考AMD平臺的BIOS文件。要分解的BIOS檔選了當下較新的X38晶片組平臺的ex38dq6.f2 這個BIOS檔,而BIOS的檔是ma79xds4.f4,這是AMD的最新的7系晶片組其中的790X晶片組平臺。好,下面開始進行分解ex38dq6.f2這個BIOS檔。

一、工具的使用
1、Ex38dq6.f2是Award Bios,有一個圖形化的BIOS編輯軟體awdbedit,可以很方便的將BIOS的元件分解出來。
2、通用的BIOS編輯軟體cbrom,這裡使用的是cbrom182版本。下面是使用cbrom182顯示BIOS元件的清單,命令列下使用:Cbrom182 ex38dq6.f2 /D,結果如下(部分):

******** ex38dq6.f2 BIOS component ********
No. Item-Name Original-Size Compressed-Size Original-File-Name
================================================================================
0. System BIOS 20000h(128.00K)15478h(85.12K)ex38dq6.BIN
1. XGROUP CODE 0FC40h(63.06K)0B0ECh(44.23K)awardext.rom
2. ACPI table 04E16h(19.52K)0193Ch(6.31K)ACPITBL.BIN
3. EPA LOGO 0168Ch(5.64K)0030Dh(0.76K)AwardBmp.bmp
4. GROUP ROM[18] 031D0h(12.45K)0225Ah(8.59K)ggroup.bin
5. YGROUP ROM 0C180h(48.38K)066E4h(25.72K)awardeyt.rom
6. GROUP ROM[ 0] 08210h(32.52K)0303Dh(12.06K)_EN_CODE.BIN
7. PCI ROM[A] 10000h(64.00K)09DBEh(39.44K)ICH9RAID.BIN
8. PCI ROM 03600h(13.50K)02553h(9.33K)ICH8AHCI.BIN
9. PCI ROM[C] 07A00h(30.50K)04479h(17.12K)JMB59.BIN
10. MINIT 08220h(32.53K)0824Fh(32.58K)DDR2_MRC.X38
11. PCI ROM[D] 0C800h(50.00K)079FDh(30.50K)rtegrom.lom
12. LOGO1 ROM 00B64h(2.85K)00520h(1.28K)dbios.bmp
13. LOGO BitMap 4B30Ch(300.76K)07EEEh(31.73K)x48dq6.bmp
14. GV3 01EFDh(7.75K)00B66h(2.85K)PPMINIT.ROM
15. OEM0 CODE 028ABh(10.17K)01E1Bh(7.53K)SBF.BIN
(SP) NCPUCODE 1D000h(116.00K)1D000h(116.00K)NCPUCODE.BIN

Total compress code space = E5000h(916.00K)
Total compressed code size = 75C8Dh(471.14K)
Remain compress code space = 6F373h(444.86K)

清單: 2.1

整個ex38dq6.f2 檔1M大小,包含了16個元件,最後的NCPUCODE.BIN元件,是虛擬的或者說物理上不存在,用awdbedit軟體分解不包括這個元件,實際上只有15個真實元件,這些元件全都是經過壓縮的。第2列是元件的名字,第3列是元件真實的大小,第4列是元件中部分壓縮的資料在ex38dq6.f2檔中的大小,最後1列是分解後元件存在磁片上的物理檔案名,以ex38dq6.bin為例,這個元件真實的大小為128K,其中85.12K是壓縮部分,其餘的以純代碼形式分佈在FE000 ~ FFFFF區域,典型地:第一條far jmp就分佈在這個區域。
ex38dq6.BIN 是BIOS的主體組件。
awardext.rom、awardeyt.rom 是BIOS的擴展部分。
ACPITBL.BIN 是供ACPI所使用的低級部件,可供作業系統使用。
PCI ROM 是 PCI 設備的一些元件。
還有一些顯示的BMP圖片
其餘組件不詳,有待瞭解
3、使用cbrom182來分解元件的方法:
Cbrom182 ex38dq6.f2 /XGROUP extract 分解出 awardext.rom
Cbrom182 ex38dq6.f2 /ACPI extract 分解出 ACPITBL.BIN
如此類推,可以逐步分解出各個元件,但是,SYSTEM BIOS元件,也即是 ex38dq6.bin 這個元件,我怎麼試也沒分解出來,所以用以下推薦的方法分解。

4、推薦分解BIOS元件的方法
使用圖形化的BIOS編輯軟體awdbedit可以很方便簡單分解全部的元件。運行awdbedit軟體,打開ex38dq6.f2,忽略掉一些警告資訊,進入後,選擇 [Actions] –> [Extract All] 就可以分解出全部的元件。

二、BIOS元件位置分析
1、ex38dq6.f2檔共1M大小,除了包含各個BIOS元件外,還充斥著大量的“填充碼”,這些“填充碼”是FF位元組以及00位元組,主要用來分隔各個元件,以及填充檔。
2、壓縮元件是以LZH形式壓縮,每個壓縮元件以“-lh5-”開頭,十六進位碼形式為 2D 6C 68 35 2D,這是壓縮組件的戳記,因此,在BIOS檔中只要尋找到這個戳記就可以區分開每個元件。
seg000:0000 24 F7 2D 6C 68 35 2D 50 54 01 00 00 00 02 00 00 $?lh5-PT ... ..
seg000:0010 00 00 50 20 01 0B 65 78 33 38 64 71 36 2E 42 49 ..P
ex38dq6.BI
seg000:0020 4E 24 D3 20 00 00 2D 20 8F 77 BF 74 89 29 BB AA N$?..- 弚縯?華
seg000:0030 7F 33 33 37 37 4D 07 73 55 45 55 78 35 91 D5 66 3377MsUEUx5懻f
seg000:0040 85 B7 54 49 34 52 21 0E 9B A5 10 91 11 BC 1D 28 叿TI4R!
洢 ??(
seg000:0050 B1 2A 66 A0 DD 5B BB BA 9C 0D 51 0C C5 17 AA F2 ?f犦[緩?Q
?
seg000:0060 FB DD BC AC AD 34 F1 55 DB 53 CC 03 DD A6 86 30 棘?馯跾?葒?
seg000:0070 2A CF 42 B5 DC 53 52 22 43 F0 75 84 66 40 00 77 *螧弟SR"C饀刦@.w
seg000:0080 7F FE 66 83 37 77 79 E7 9E BC F6 FF BD 7A FD EE �?wy鐬薦�絲
seg000:0090 BE FF 04 3E F7 76 B2 49 1B 6D C9 D1 4B 2D 15 A0 ? >鱲睮 m裳K- ?
seg000:00A0 AE 84 C4 52 58 5F FF CF ED 24 AC C1 42 64 1F F0 畡腞X_�享$Bd­?
seg000:00B0 BF 45 55 49 0A A2 CE C2 97 58 58 AF 0E 62 22 84 縀UI⑽聴XX?b"?
seg000:00C0 7E CF 94 2F E7 24 F7 E3 CE 0F 55 B8 E0 94 E0 D5 ~蠑/?縻?U膏斷?
seg000:00D0 D1 BE E0 9E FB 99 C1 F8 3B 86 C5 B8 86 6C 6B 85 丫酁麢柳;喤竼lk?
seg000:00E0 88 B3 F7 05 5A F0 BA CB C3 2E 5F 89 F8 AF ED B2 埑?Z鷙嗣._夬?
seg000:00F0 91 9C 42 50 B7 CA 60 34 B6 4A 55 8C 65 D3 8E EA 憸BP肥`4禞U宔訋?
seg000:0100 6A 5D E1 4F 7E DB 97 7F 4C A0 AE 9E 15 B7 8E 86 j]酧~蹢L牣?穾?

清單 2.2

3、以ex38dq6.f2為例,用十六制編輯軟打開,從00000000開始到000FFFFF共1M的大小,每個壓縮元件在ex38dq6.f2的物理位置如下:
0. 0 ~ 15477: System Bios (ex38dq6.bin)
1.15478 ~ 20563: XGROUP CODE(awardext.rom)
2.20564 ~ 21E9F: ACPI table(ACPITBL.BIN)
3. 21EA0 ~ 221AC: EPA LOGO(awardBmp.bmp)
4.221AD ~ 24406: GROUP ROM[18](ggroup.bin)
5.24407 ~ 2AAEA: YGROUP ROM(awardeyt.rom)
6.2AAEB ~ 2DB27: GROUP ROM[0](_EN_CODE.BIN)
7.2DB28 ~ 378E5: PCI ROM[A](ICH9RAID.BIN)
8.378E6 ~ 39E38: PCI ROM(ICH8AHCI.BIN)
9.10. 39E39 ~ 46500: PCI ROM[C]、MINIT(JMB59.BIN、DDR2_MRC.X38)
11.46501 ~ 4DEFD: PCI ROM[D](rtegrom.lom)
12.4DEFE ~ 4E41D: LOGO1 ROM(dbios.bmp)
13.4E41E ~ 5630B: LOGO BIGMAP(X48DQ6.bmp)
14.5630C ~ 56E71: GV3(PPMINIT.ROM)
15.56872 ~ 58C8C: OME0 CODE(SBF.BIN)

以上是各個壓縮元件在BIOS檔中的物理位置,從58C8D ~ FDFFF 這段區間中混合著一些資料,還在大量充斥著“填充碼”,沒有什麼實際的意義,或者說:沒看到什麼實際意義。從 FE000 ~ FFFFF 這段區間中,包含一些非壓縮的純二進位碼,其中有重要的BOOTBLOCK,以及一些初始化代碼。也包含著大量的“填充碼”。這些純代碼分散分佈在這個區間,純代碼與部分壓縮元件的混合在一起,幾乎很難區分哪些是純代碼,哪些是壓縮資料,指令:jmp far ptr 0F000:0E05B 與其它資料混合在一起,如下清單2.2所示:
seg000:FFB25 db 0C3h ; ?
seg000:FFB26 db 66h ; f
seg000:FFB27 db 0EFh ; ?
seg000:FFB28 db 8Bh ; ?
seg000:FFB29 db 0D7h ; ?
seg000:FFB2A db 8Eh ; ?
seg000:FFB2B db 0D9h ; ?
seg000:FFB2C ; ---------------------------------------------------------------------------
seg000:FFB2C jmp far ptr 0F000h:0E05Bh
seg000:FFB2C ; ---------------------------------------------------------------------------
seg000:FFB31 db 0
seg000:FFB32 db 0
seg000:FFB33 db 0
seg000:FFB34 db 0
seg000:FFB35 db 0
seg000:FFB36 db 0
seg000:FFB37 db 0
seg000:FFB38 db 0
seg000:FFB39 db 0

清單 2.3

4、BIOS的主體文件 ex38dq6.BIN的大小是128K,正好映射到系統位址空間的FFFE_0000 ~ FFFF_FFFF(E_0000 ~ F_FFFF)共128K的空間上。
當分解出BIOS主體元件ex38dq6.BIN後,這條指令就在F000:FFF0 的位置上,如下清單2.3所示:

seg000:FFFEC db 80h ; €
seg000:FFFED db 1
seg000:FFFEE db 0Ch
seg000:FFFEF db 89h ; ?
seg000:FFFF0 ; ---------------------------------------------------------------------------
seg000:FFFF0 jmp far ptr 0F000h:0E05Bh
seg000:FFFF0 ; ---------------------------------------------------------------------------
seg000:FFFF5 db 30h ; 0
seg000:FFFF6 db 32h ; 2
seg000:FFFF7 db 2Fh ; /
seg000:FFFF8 db 32h ; 2
seg000:FFFF9 db 37h ; 7
seg000:FFFFA db 2Fh ; /
seg000:FFFFB db 30h ; 0
seg000:FFFFC db 38h ; 8
seg000:FFFFD db 0
seg000:FFFFE db 0FCh ; ?
seg000:FFFFF db 0B3h ; ?
seg000:FFFFF seg000 ends

清單 2.4

在ex38dq6.BIN 檔中的FFFF0位置對應著物理FFFFFFF0這個位址上,第1條指令是far jmp,跳轉到BOOTBLOCK中,通常在這條指令的周圍是些有意議的字元描述,如:02/27/08這是BIOS日期,在far jmp 下麵處於BIOS尾端。

三、不同BIOS檔之間的異同

1、結構不同,AMI和award的BIOS是有差別的。
2、BIOS檔的大小不同,一般的BIOS檔大小為512K,以ma79xds4.f4為例,它是512K,ex38dq6.f2是1M,但結構上差什麼差異,下面是ma79xds4.f4的結構:

No. Item-Name Original-Size Compressed-Size Original-File-Name
================================================================================
0. System BIOS 20000h(128.00K)13944h(78.32K)ma79xds4.BIN
1. XGROUP CODE 0F7D0h(61.95K)0AB7Bh(42.87K)awardext.rom
2. ACPI table 06391h(24.89K)02B35h(10.80K)ACPITBL.BIN
3. EPA LOGO 0168Ch(5.64K)0030Dh(0.76K)AwardBmp.bmp
4. GROUP ROM[18] 03340h(12.81K)02339h(8.81K)ggroup.bin
5. YGROUP ROM 0B310h(44.77K)05023h(20.03K)awardeyt.rom
6. GROUP ROM[ 0] 07100h(28.25K)02C90h(11.14K)_EN_CODE.BIN
7. PCI ROM[A] 0C800h(50.00K)0AC37h(43.05K)sata22.bin
8. OEM1 CODE 0AE4Fh(43.58K)06B6Dh(26.86K)ui22.bin
9. PCI ROM[B] 0A800h(42.00K)06007h(24.01K)RTLGPXE.LOM
10. LOGO1 ROM 00B64h(2.85K)00520h(1.28K)dbios.bmp
11. OEM0 CODE 028ABh(10.17K)01E1Bh(7.53K)SBF.BIN
12. GV3 088C6h(34.19K)026FBh(9.75K)AGESACPU.ROM
13. MINIT 11B80h(70.88K)11BB3h(70.92K)MEMINIT.BIN
14. HTINIT 04BC0h(18.94K)04BF0h(18.98K)HT.DLL
15. 2 PE32 in MB 00552h(1.33K)00582h(1.38K)HT32GATE.BIN
(SP) NCPUCODE 04000h(16.00K)04000h(16.00K)NCPUCODE.BIN

Total compress code space = 63000h(396.00K)
Total compressed code size = 621F3h(392.49K)
Remain compress code space = 00E0Dh(3.51K)

清單 2.5

上面清單所示,與ex38dq6.f2的結構一樣,每個元件都有一部分是經過壓縮的。
--> 閱讀更多...

●跟著流程走(一):far jmp後發生什麼?




跟著流程走(一):far jmp後發生什麼?
一、所需材料
1、主要BIOS :ex38dq6.f2 :技嘉主機板上Intel X38 MCH + ICH9 平臺。
備用BIOS:ma79xds4.f4 : 技嘉主機板上AMD 790X 北橋 + SB600 南橋平臺。
2、cbrom:這是一個BIOS編輯工具,這裡所用的是cbrom182版本
3、lha2.55:LHA格式的解壓工具。
4、awdbedit:award bios 的圖形化編輯工具,方便簡單。
5、hex workshop:一個十六進位編輯工具,簡單小巧。
6、IDA:一個反彙編工具,這裡使用的是IDA 5.2版本

二、所需知識
1、組合語言:這是必備的知識,彙編掌握的程度和理解能力成正比。
2、機器語言:這個不是必需的,但推薦能夠讀懂機器語言,某些場合下當組合語言也陷入窘境時,機器語言是唯一的解釋手段。
3、x86體系知識:具體可以查看相關的Intel 或 AMD 手冊
4、ISA/PCI 匯流排知識:可以查看相應的 ISA/PCI Specification
5、north/south bridge 知識:Intel 現在以MCH代稱north bridge,ICH代稱south bridge,可以查看相應的 datasheet


接下來用IDA pro打開ex38dq6.BIN觀察,這個是BIOS的主體檔,跟著流程走,看看far jmp後BIOS做什麼工作。
1、第一條指令 jmp far ptr F000:E05B 經過幾個跳轉,跳到F000:F46C處
2、以下是F000:F46C的代碼:
seg000:FF46C cli
seg000:FF46D cld
seg000:FF46E xchg bx, bx
seg000:FF470 smsw ax
seg000:FF473 test al, 1
seg000:FF475 jz short near ptr 0F480h
seg000:FF477 cli
seg000:FF478 mov al, 0FEh ; '?
seg000:FF47A out 64h, al ; AT Keyboard controller 8042.
seg000:FF47A ; Resend the last transmission
seg000:FF47C cli
seg000:FF47D hlt
--------------------------------------------------------------
取機器狀態字,也就是CR0寄存器,測試CR0.PE是否為1,判斷CPU是否處於真實模式狀態,若不是則停機。
若處於真實模式轉到F000:F480繼續處理。
3、轉到F000:F480又經過一道跳轉,來到F000:E043進行處理
4、下麵是F000:E043的代碼:

seg000:FE043 mov al, 8Fh ; '? ; disable NMI# and get 0Fh offset register
seg000:FE045 out 70h, al ; CMOS Memory:
seg000:FE045 ;
seg000:FE047 out 0EBh, al
seg000:FE049 in al, 71h ; get OFh offset register data
seg000:FE04B out 0EBh, al
seg000:FE04D or al, al ; is RESET ?
seg000:FE04F jmp near ptr 0F483h

在這裡,取 CMOS RAM 中位於0F處1個位元組的資料,通過測試這個位元組是否為0,判斷是否屬正常啟動。
5、正常啟動的話,調用F000:54DE這個子過程進行處理,否則跳到F000:3468。
二、下面看看F000:54DE的處理,以下是第二張流程圖:
下面proc_F54DE的代碼:

seg000:F54DE mov ax, 0
seg000:F54E1 mov es, ax
seg000:F54E3 cmp word ptr es:472h, 1234h
seg000:F54EA jnz short near ptr 54F8h
seg000:F54EC mov al, 8Fh ; '?
seg000:F54EE out 70h, al ; CMOS Memory:
seg000:F54EE ;
seg000:F54F0 out 0EBh, al
seg000:F54F2 mov al, 0AAh ; '?
seg000:F54F4 out 71h, al ; CMOS Memory:
seg000:F54F4 ;
seg000:F54F6 out 0EBh, al
seg000:F54F8 mov dx, 3C4h
seg000:F54FB mov al, 1
seg000:F54FD out dx, al ; EGA: sequencer address reg
seg000:F54FD ; clocking mode. Data bits:
seg000:F54FD ; 0: 1=8 dots/char; 0=9 dots/char
seg000:F54FD ; 1: CRT bandwidth: 1=low; 0=high
seg000:F54FD ; 2: 1=shift every char; 0=every 2nd char
seg000:F54FD ; 3: dot clock: 1=halved
seg000:F54FE inc dl
seg000:F5500 in al, dx ; EGA port: sequencer data register
seg000:F5501 or al, 20h
seg000:F5503 out dx, al ; EGA port: sequencer data register
seg000:F5504 call near ptr 76FBh
seg000:F5507 retn

1、在BIOS資料區的0472處存放著一個重定標誌:
seg000:F54E3 cmp word ptr es:472h, 1234h
通過比較 [0472] 是否1234h,標誌1234h是一個暖開機標誌位元,機器暖開機時,例如:按下CTRL+ALT+DEL 三個鍵時,由鍵盤中斷處理常式在[0472]處寫標誌1234h。
2、是暖開機的話,將寫入AA標誌到CMOS RAM 的0F處。
3、接著設置EGA相應的工作狀態。
4、在proc_F76FB過程裡置計時器1的狀態。
5、最後調用過程proc_F2941進行晶片組的初始化。

三、下面是本站節的重點,初始化某部分晶片組,下面是流程圖:
1、下面重點理解 write_pci_byte這個BIOS提供的rontine,代碼如下:

seg000:FF798 xchg ax, cx ; write_byte routine
seg000:FF799 shl ecx, 10h
seg000:FF79D xchg ax, cx
seg000:FF79E mov ax, 8000h ; Bus 0
seg000:FF7A1 shl eax, 10h
seg000:FF7A5 mov ax, cx
seg000:FF7A7 and al, 0FCh
seg000:FF7A9 mov dx, 0CF8h ; config_address register
seg000:FF7AC out dx, eax
seg000:FF7AE add dl, 4 ; config_data register
seg000:FF7B1 mov al, cl
seg000:FF7B3 and al, 3
seg000:FF7B5 add dl, al
seg000:FF7B7 mov eax, ecx
seg000:FF7BA shr eax, 10h
seg000:FF7BE out dx, al
seg000:FF7BF retn

將這個routine功能簡化為C代碼形式來看比較直觀:
void wirte_pci_byte(int offset_number, int mask)
{
if (number == -1)
jmp_7666();

do_wirte_pci_byte(offset_number, mask);
}

這段routine固定寫PCI的Bus0,Device0,Function0,offset 值放在cx中,由調用者傳來,置什麼值放在al寄存器,這是1個位元組的值。Bus0,Dev0,Fun0是hostbrige控制器(NorthBridge),也即是DRAM控制器的地址所在。這段代碼是典型的寫PCI設置的手法。PCI設置位址送入config_address_register中,然後往config_data_register裡寫資料,這個PCI設備位址將映射到PCI設備的寄存器,如前面介紹的位址空間圖所示,PCI設備位址範圍是E000_0000 ~ EFFF_FFFF,這段空間提交到相應的PCI設備。
2、現在回過頭來看調用者,cx=95,al=33 這個參數傳給 write_pci_byte。Offset是95,mask碼是33。Offset 95在write_pci_byte將被置為94,這將是DRAM控制器的PAM4寄存器,PAM4寄存器控制D_8000 ~ D_FFFF記憶體空間的屬性。寫入33,結果是:將這段空間置為read/write屬性,這將是所有訪問這段空間的操作會提交到DRAM。而不再是ROM。

3、Offset 96的結果和offset 95一樣,在write_pci_byte的遮罩中被置為offset 94。
--> 閱讀更多...

●memtest86+教學 Part7

這一次我想從簡單部份說起;學code得基本原則就是debug,因此要debug就必需將我們不懂的地方把它show到螢幕的畫面上。所以我們先介紹以下這個函式:
/*
* Print characters on screen
*/
void cprint(int y, int x, const char *text)
{
register int i;
char *dptr;

dptr = (char *)(SCREEN_ADR + (160*y) + (2*x));
for (i=0; text[i]; i++) {
*dptr = text[i];
dptr += 2;
}
tty_print_line(y, x, text);//印出的UART
}
這是一個印出文字(字串)到螢幕上的Routine,相信它應該非常簡單,SCREEN_ADR0xb8000,這個位址就是bios映射到vga的彩色文字資料區;對這一區的記憶體讀寫就等同於對螢目frame buffer讀寫;相關資訊可參考這個網址。
接著我們去回想一下do_test有呼叫一個routine:init()
void init(void)
{
int i;

outb(0x8, 0x3f2); /* Kill Floppy Motor */

/* Turn on cache */
set_cache(1);

/* Setup the display */
display_init();

/* Determine the memory map */
if ((firmware == FIRMWARE_UNKNOWN) &&
(memsz_mode != SZ_MODE_PROBE)) {
if (query_linuxbios()) {
firmware = FIRMWARE_LINUXBIOS;
}
else if (query_pcbios()) {
firmware = FIRMWARE_PCBIOS;
}
}

mem_size();

/* setup pci */
pci_init();

/* setup beep mode */
beepmode = BEEP_MODE;

v->test = 0;
v->pass = 0;
v->msg_line = 0;
v->ecount = 0;
v->ecc_ecount = 0;
v->testsel = -1;
v->msg_line = LINE_SCROLL-1;
v->scroll_start = v->msg_line * 160;
v->erri.low_addr.page = 0x7fffffff;
v->erri.low_addr.offset = 0xfff;
v->erri.high_addr.page = 0;
v->erri.high_addr.offset = 0;
v->erri.min_bits = 32;
v->erri.max_bits = 0;
v->erri.min_bits = 32;
v->erri.max_bits = 0;
v->erri.maxl = 0;
v->erri.cor_err = 0;
v->erri.ebits = 0;
v->erri.hdr_flag = 0;
v->erri.tbits = 0;
for (i=0; tseq[i].msg != NULL; i++) {
tseq[i].errors = 0;
}
if (dmi_initialized) {
for (i=0; i <> 0) {
dmi_err_cnts[i] = 0;
}
}
}

cprint(LINE_CPU+1, 0, "L1 Cache: Unknown ");
cprint(LINE_CPU+2, 0, "L2 Cache: Unknown ");
cprint(LINE_CPU+3, 0, "Memory : ");
aprint(LINE_CPU+3, 10, v->test_pages);
cprint(LINE_CPU+4, 0, "Chipset : ");

cpu_type();

/* Find the memory controller (inverted from standard) */
find_controller();

if (v->rdtsc) {
cacheable();
cprint(LINE_TIME, COL_TIME+4, ": :");
}
cprint(0, COL_MID,"Pass %");
cprint(1, COL_MID,"Test %");
cprint(2, COL_MID,"Test #");
cprint(3, COL_MID,"Testing: ");
cprint(4, COL_MID,"Pattern: ");
cprint(LINE_INFO-2, 0, " WallTime Cached RsvdMem MemMap Cache ECC Test Pass Errors ECC Errs");
cprint(LINE_INFO-1, 0, " --------- ------ ------- -------- ----- --- ---- ---- ------ --------");
cprint(LINE_INFO, COL_TST, " Std");
cprint(LINE_INFO, COL_PASS, " 0");
cprint(LINE_INFO, COL_ERR, " 0");
cprint(LINE_INFO+1, 0, " -----------------------------------------------------------------------------");

for(i=0; i < style="font-weight: bold;">cprint(i, COL_MID-2, " ");
}
footer();
// Default Print Mode
// v->printmode=PRINTMODE_SUMMARY;
v->printmode=PRINTMODE_ADDRESSES;
v->numpatn=0;
find_ticks();
}
這些粗體字就是我會講解的重點;首先說明set_cache
void set_cache(int val)
{
extern struct cpu_ident cpu_id;//cpu_id這個資料結構在head.S中已初始化完成
/* 386's don't have a cache */
if ((cpu_id.cpuid lss 1) && (cpu_id.type == 3))
{
cprint(LINE_INFO, COL_CACHE, "none");
return;
}
switch(val)
{
case 0:
cache_off();
cprint(LINE_INFO, COL_CACHE, "off");
break;
case 1:
cache_on();
cprint(LINE_INFO, COL_CACHE, " on");
break;
}
}

static inline void cache_on(void)
{
asm(
"push %eax\n\t"
"movl %cr0,%eax\n\t"
"andl $0x9fffffff,%eax\n\t" /* Clear CD and NW */
"movl %eax,%cr0\n\t"
"pop %eax\n\t");
}
這個組合語言是GCC-Inline-Assembly,請自行參考語法說明。打開CACHE快取才可使CPU存取RAM的速度加快;因為匯流排,即使是跑DUAL CHENNEL,也不會比CPU快,因此快取越大,更能提升CPU效能。但要知道一點,這軟體主要是測試記憶體,萬一CPU快取本身有問題,就很難去測試記憶體真正的PASS或FAIL。
--> 閱讀更多...

2009年3月27日 星期五

●有關smm模式及big real mode

有沒有32位真實模式,why?
在80286之後的機器上,在真實模式下已經可以部分使用32位元資料,如寄存器可以用eax等,但根本的問題是不能定址32位的位址空間,但原因不是在真實模式下不能使用32位元的定址方式,而是位址空間被“封閉”了。
如果你寫一段程式,選進入保護模式,將gs,fs等寄存器設置為可以定址全部記憶體空間,然後返回到真實模式,只要你不刷新gs /fs,則始終可以通過它們訪問全部記憶體,這表明在真實模式下是具備32位能力的,只是不太好用罷了。
Q:請問為什麼不太好用啊?A:你要在真實模式和保護模式下頻繁地切換,累啊。
因為一般來說程式中總是需要偶爾改動一下ds,es,cs這些常用的段寄存器,因此在真實模式下,在大多數時候還是受到限制,上面回復的方法只是突破了資料段的限制,使資料段可以達到4G,但程式碼片段還是不行。
intel 的文檔裡說的很清楚。SMM模式特意設計成只能讓系統的固件使用,不能被使用者程式和系統程式進入。進入SMM模式的方法是給SMI#管腳輸入一個電平信號 或者通過APIC給匯流排發一個SMI消息。你在看看smm那章,smm與其他模式切換是比較特殊,但關於big real mode 的介紹也只出現在intel手冊的這章中。smm模式使用真實模式的段式管理,但他的資料段描述符是4g的。
intel cpu smm模式與其他模式的切換只要你有晶片組的手冊就能知道如何切換(處於特權級0),cyrix的cpu 通過寄存器 0x22 0x23也可以切換。

問題越來越有趣了。flat(big) real mod is not system manager mode。SMM的進入方法我想我也沒有理解錯,intel x86進入SMM的唯一方式就是給SMI#引腳輸入信號,具體可以通過對ACPI或者fireware進行程式設計實現。進入SMM方式比進入big real mode要複雜,一方面要對APIC或者fireware程式設計,另一方面要設置好SMM的執行環境。intel文檔中也明確說明SMM方式特意設計成只能 讓fireware執行(當然,系統程式通過APIC也是可以實現的)。
在網上搜集了一些資料是說flat(big) real mode的,在486S後確實存在這種32位的真實模式,其位址是32位元的,但是指令預設是16位元的,如果執行32位元指令要在前面加首碼。一旦進入flat real mode,也沒有必要去更改CS,DS,FS,ES等段寄存器了,因為記憶體是平坦模式,基底位址是00000000,界限是4G,所以可以放心使用,關鍵問 題是16位元指令和32位元指令混合使用會產生麻煩。Windows 3.1就是使用這種模式。但很奇怪,intel的手冊裡面沒有提到過flat(big) real mode,也許正如文中所說,當Pentium引入時,big real mode再也沒有什麼吸引力了,以至於flat real mode只是曇花一現,這種模式成了intel公司的X file.

是的是的,書上說的沒錯,你理解也正確。我最開始的意 思就是big real mode在intel的手冊也只有smm這章有過介紹。big real mode有多中叫法:unreal mode ,falt real mode,其實都是一個意思,在real mode 下使用4g記憶體。不過切換到smm 模式我個人並不覺得有多困難,intel 晶片組的手冊是開放的!切換到smm只要通過晶片組允許0xA0000,然後寫io 埠0xb2產生smi中斷就切換到smm了,退出用rsm指令,smm是個有趣的模式,在這個模式裡你可以看到Descriptor Cache 的內容!

--> 閱讀更多...

2009年3月26日 星期四

●X86開機時的狀況

開機後,CPU重置,從位址FFFFFFF0取第一個命令,這個位址正好落在ROM BIOS中。該位址內容一定是一個JMP指令,系統便跳轉到該JMP指令所指的地方。
1‧而這裡之後我開始不明白了。JMP要跳轉到的位置是在高地址(4G末端)Flash Rom BIOS中還是在低地址(1M末端)的shadow BIOS呢?
2‧位於低地址(1M處)的(BIOS shadow)是從Flash BIOS拷貝而來呢,還是沒有任何拷貝過程僅僅利用位址映射到原Flash BIOS中的呢?
3‧無論是拷貝還是映射,記憶體位址空間上ROM BIOS映射區只有 0xF0000~0x100000之間的64KB。而ROM本身有可能大於2M。那映射的應該是原BIOS程式的一部分,那麼是哪一部分呢?對這個問題的回答需要闡明機關概念:
1.機器加電時,記憶體控制器還沒有初時化,記憶體是不可用。
2.機器加電時,對CPU的指令的解碼不是北橋,CPU發出的位址被傳遞到南橋並由FHW(Firmware Hub)解碼到BIOS ROM晶片(Flash)。在加電時一直到引導進程初,BIOS的E段(0xE0000~0xEFFFF)和F段(0xF0000~0xFFFFF)和4G記憶體頂端的對應段0xFFFE0000~0xFFFEFFFF和0xFFFF0000~0xFFFFFFFF都被FWH解碼到BIOS的ROM晶片的兩個64區域。即在啟動階段訪問0xE0000~0xEFFFF和0xFFFE0000~0xFFFEFFFF是同一個BIOS區域,訪問0xF0000~0xFFFFF和0xFFFF0000~0xFFFFFFFF是同一個BIOS區域。
3.機器加電時,CS段寄存器值為0xF000,EIP值為0x0000FFF0,但CPU的取的位址是段寄存器不可見的部分(影子寄存器)加上偏移部分,此時影子寄存器的值為0xFFFFFFF0。所以CPU執行的第一條指令是0xFFFFFFF0(復位向量),通常在BIOS ROM對應的指令是一個跳轉指令JMP F000:E05B,當取出跳轉完成後,由於CS段的影子寄存器刷新並重新載入,下一條指令位址是0xFE05B。不過這條指令仍然從BIOS ROM裡取得。
4.關於shadow BIOS,BIOS程式通常是壓縮的,在系統初始化階段,BIOS會解壓BIOS Image到RAM中,然後程式設計北橋控制器對0xE0000~0xFFFFF置為write only,這樣對該區域的寫被傳遞到DRAM裡,然後把解壓的BIOS拷貝到E段和F段。最後重新程式設計北橋控制器對0xE0000~0xFFFFF置為read only。對於PCI ROM BIOS,BIOS會把每個卡上的ROM拷貝到0xC0000~0xDFFFF然後執行他們的初時化代碼。5.BIOS ROM可以很大,但不都是可執行的,如含有ACPI Table等,開始解壓到0xE0000~0xFFFFF只是其中一部分,在啟動過程中還需要從BIOS ROM解壓代碼到RAM中,並覆蓋其中不需要的代碼。這個就好像BIOS ROM是硬碟(不過可用直接訪問),真正執行的代碼在RAM中一樣。硬碟可以很大但RAM小,這也就是程式的局部性原理。
這個網址也是相關議題的論述:X86 開機流程小記BIOS 探索之旅可以比較一下是否有差異點。

BIOS的幾個模組中有部分是壓縮的,有部分是 pure binary,純代碼和壓縮代部參雜在一起。
BIOS有部分是 routine 元件,它是 pure binary,其它就包括初始化(晶片組、DRAM等)模式,解壓routine元件、還有就是CPU 的微代碼update模式等等,提供BIOS運行期間的一些函式呼叫,解壓rontine的作用就是解壓壓縮組件,將它們放入相應的memory中。還有 一件重要的事情是,建立一個 interrupter vector 及 interrupt service routine。

bios啟動ram記憶體初始化前bios是否在rom中運行,這樣的話rom的地址會不會跟ram地址衝突?
北橋晶片有 Shadow RAM 功能,ROM 和 RAM 都會映射到同一段位址,但是根據讀寫信號的不同轉到 ROM 或者 RAM去。
第一條 long jmp 指令,也就成為 x86 pc 機的固有約定或者說是規範吧。

其實主要的原因是相容!追溯到最早 808X 系列處理器,8080 是 16 位 address bus, 8086 及 8088 改進為 20 進 address bus,整個 808X 系列處理就是整個 x86 架構的始祖。定址空間 00000 ~ FFFFFh 也就是 1M 的空間。
當時 IBM 決定使用 8086 處理作為 IBM PC 機,故事就從那裡開始,BIOS 這個名詞也就是 IBM 發明出來的,IBM 搞出來的 BIOS 定位在 8086 處理器的定址高端,也就是 F0000 到 FFFFF 區域。從 386 開始,address bus 增加到 32 條,定址範圍從 0 ~ FFFFFFFFh,BIOS 的定位也在 4G 的高端FFFF0000 ~ FFFFFFFFh,但為了相容,對 F0000 ~ FFFFF 的訪問被映射到 FFFF0000 ~ FFFFFFFFh,這是從理論上定義的。
從物理上來講,F0000 ~ FFFFFh 映射到 FFFF0000 ~ FFFFFFFFh 靠硬體來保證,在位址解碼時,F0000 ~ FFFFFh 與 FFFF0000 ~ FFFFFFFFh 會被解碼到同一個區域。現在的晶片組提供的廠商有很多,如:Intel,AMD,nvidia,VIA,SIS 等,它們的解碼實現方法可能會不同,但都要保證這個所謂的“別名”機制。
Intel 實現是:MCH 將 C_0000 ~ F_FFFF 這段區域定義為 PAM(Programed attribute memory),分 disable,read-only ,wirte-only,read/write 四種屬性,初始屬性是 disable,也即是無用,因此這段區域將被送去 ICH 解碼,FFE0_0000 ~ FFFF_FFFFh 的也被 ICH 解碼,ICH 轉交 LPC bridge 處理,它們被解碼為同一區域。
AMD 實現是:無論是 C_0000 ~ F_FFFF,還是 FFFC_0000 ~ FFFF_FFFF 最終結果都將送到 LPC bus 上的 FFFC_0000 ~ FFFF_FFFF 物理位址上。其它的廠商實現也大體這樣。

因此:long jmp 後,轉到 FE05B(jmp far ptr F000:E05B)執行,它將被映射到物理位址 FFFFE05B 上,這還是 BIOS 所在的 ROM 中。第一條指令的 FFFFFFF0 與 第二條的 FE05B 都是在 BIOS 的 ROM 上。
--> 閱讀更多...

2009年3月24日 星期二

●繼續閱讀懶人加強版

繼續閱讀懶人加強版

好用ㄉ部落格工具。

--> 閱讀更多...

●描述符表和描述符快取記憶體




在80x86的CPU裡,描述符的概念實在是太重要了。
在真實模式下,大家都知道物理位址是由段位址和偏移位址兩部分組成,其公式如下:
物理位址 = 段位址 × 16 + 偏移位址
或者:物理位址 = 段位址 << 4 + 偏移位址
其結果都是一樣的,由於段位址和偏移位址的長度都是16位元,所以這種方式能夠表達的最大位址為:ffff:ffffH,也就是10ffefH,大致是 1088KBytes,有由於8086CPU的位址線只有20位,所以在8086上實際的定址能力僅為1024KBytes,在80286和 80386CPU上,通過A20的使用,可以實際定址到1088KBytes,這個問題在《關於A20 gate》的文章中有過介紹。
從80386開始,CPU的位址匯流排已經到了32位元,但只有在保護模式下才能真正地享受32位的高性能,實際上在保護模式下,CPU也是使用段:偏移位址 的方式來定址的,只是這裡面有兩點區別,1-偏移位址可以是16位也可以是32位;2-段寄存器中的內容和在真實模式下的含義完全不一樣,而且已經不是組成 物理位址的一部分。顯然,偏移位址的這種變化是很容易理解的,無需更多地解釋,所以,保護模式和真實模式相比,重要的區別就是段寄存器中內容的區別了。
那麼,這個段寄存器中到底存的是什麼東東呢?它存的是某個描述符在描述附表中的偏移值,這是什麼意思呢?一個描述附表中有很多描述符,每個描述符的長度都 相同,假如第一個描述符我們編號是0的話,那麼第n個描述符的編號就是n-1,這個n-1就是所謂的描述符索引,所謂“偏移”就是從描述符表起始位置起, 到我們要用的這個描述符有多少個位元組,也就是描述符索引 × 描述符長度,很顯然如果知道描述符表的起始位址,再加上段寄存器的值,我們就能找到我們要的這個描述符了。
我們先來把到此為止的問題羅列一下:
1、這個描述符是什麼東東?
2、畢竟我們是要通過段:偏移位址的形式得到物理位址,既然這個段代表一個描述符,那麼這個描述符和物理位址有什麼關係?
3、上面說到的描述符表的起始位址和描述符長度是是什麼?因為沒有這兩個東西還是找不到描述符的。
我們試著來說明這幾個問題。
首先,描述符實際上就是一個8位元組長的資料結構,它的定義如上:

每一個描述符代表一個分段(有點像真實模式下的64K分段),可以看到段基址由Byte2、Byte3、Byte4和Byte7組成,一共32bits,段 的最大長度不像在真實模式中固定為64K,而是有一個20位的段邊界(由Byte0、Byte1和Byte6的低4位組成)來設定,由於偏移位址為32位, 所以實際上段的最大長度可以達到4GBytes,段邊界的單位可以是位元組也可以是4KBytes,這取決於G=0還是G=1(Byte6的bit 7),當G=1時,段邊界的單位是4KBytes,相當於低12bits全部為1(4K剛好12bits),加上20位的段邊界,剛好為32位,最大可以 達到4GBytes;當G=0時,段邊界的單位為位元組,20位元最大為16MBytes。
在描述符的定義中還有一個訪問權位元組以及AVL、D/B等,介紹起來篇幅很長,以後有機會介紹。
說到這兒,可能大家心裡有點眉目了,段寄存器裡存放一個描述符在描述符表中的偏移,通過這個偏移可以找到相應的描述符,通過這個描述符中的段基址再加上偏移位址,就組成了物理位址,對了,基本上就是這樣,當然不會這麼簡單,其中還有很多細節,但大致原理就是這樣。
前面提到的三個問題,我們已經回答了兩個半,就是描述符是什麼,怎樣計算物理位址,還有描述符的長度是多少。最後一個問題是:描述符表的起始位址在那裡? 其實這個問題最好回答,在那篇《80386寄存器組成》的文章中提到過一個GDTR的系統表寄存器,這個描述符表的起始位址就存在這個寄存器中(這麼說不 是很完整,但不影響理解),如果你還要問,描述符表存在哪裡?如何把描述符表的起始位址放到GDTR寄存器中?這兩個問題也不難回答,描述符表可以存儲在 記憶體的任何位置;通過LGDT指令可以把描述符表的起始位址存入GDTR寄存器中。
大家有沒有感到奇怪,為什麼32位的段基址和20位的段界限在描述符中都不連續存放,這是和Intel 80x86 CPU的發展歷史分不開的,最先有保護模式的CPU是80286(現在已經不多見了),這個CPU只有24位的位址線,所以可以看到現在386的描述符中 基底位址的前24bits是連續存放的,後來在擴充時需要把基底位址變成32位元,為了和80286相容,只好分開存放了,段界限也是同樣原因造成的,所以在 80286上,一個描述符的長度雖然也是8位元組長,但它只用到了前6個位元組,到了386就全都用上了。
到這裡,應該對保護模式下記憶體的定址有了一個大致的瞭解,CPU首先通過段寄存器中的偏移值,配合GDTR寄存器中的描述符表的起始位址,找到段寄存器表示的描述符,再從描述符中獲得段基址,然後再加上偏移位址就得到了物理位址(其中許多細節省略不說)。
大家有沒有感到,CPU的定址過程很累,那麼這麼累的定址會不會導致速度變慢呢?實際上,CPU的定址過程並不像上面描述的這麼累,要解釋CPU的實際定址過程,必須要提到描述符快取記憶體(Descriptor Cache Register)。
實際上,不管是在真實模式還是在保護模式下,CPU都會把一個分段的基底位址放在一組隱藏的寄存器中,這組隱藏的寄存器,對程式師是不可見的,程式也是無法直 接存取的,但卻是實際存在的,這組隱藏的寄存器叫做描述符快取記憶體寄存器(Descriptor Cache Registers),當段寄存器的值發生變化時,段的基底位址、段的邊界以及存取屬性(存取許可權)都會被重新載入到這個段寄存器對應的快取記憶體中,為增強 性能,CPU對隨後的定址均會直接從這個快取記憶體中計算,而不會去描述符表中提取描述符,實際上,各種CPU中這個快取記憶體的內部結構是不同的,以上是幾種CPU中快取記憶體的結構圖:

不管其內部結構是怎樣的,但我們可以看到,CPU在把描述符載入進快取記憶體時並不是簡單的拷貝,而是做了一些適當的處理,比如把原來沒有連續存放的段基址 和段界限變成了連續的,把原來20bits的段界限根據G值轉換成了32bits,所以我們在快取記憶體裡看不到原來描述符裡的G標誌就是這個道理,顯然, 這些處理十分有利於快速地得出實際位址來。
前面我們提到過,CPU內部寄存器GDTR中存儲著描述符表的起始位址,細心的讀者可能會發現,《80386寄存器組成》一文中提到的GDTR寄存器有 48bits,而且分成兩部分,一部分是32bits,另一部分是16bits,這是怎麼回事呢?實際上GDTR中不僅存著描述符表的其真實位址,還存放著 描述符表的長度,其中16bits的部分就是描述符表的長度,32bits的部分就是描述符表的起始位址,按照規範,描述符表中最多可以有 8192(8K)個描述符,每個長度8個位元組,所以最大長度為64K,16bits已經足夠了,同理,段寄存仍然保持16bits長度也是足夠的。
不管是在真實模式還是在保護模式下,CPU在實際定址時都會使用這個快取記憶體。所不同的是,在真實模式下,在段寄存器的值發生變化時,僅僅把段寄存器的值 ×16(左移4位)放到快取記憶體的的基底位址位置,段界限和存取許可權總是一個固定不變的值(按照Intel的說法,PC機加電後工作在真實模式,快取記憶體中將 被置入缺省值,在真實模式下,其中的段界限和存取許可權將一直保持不變);而在保護模式下,當段寄存器發生變化時,CPU要從描述符表中載入資料到快取記憶體 中,實際上,保護模式下CPU在從描述符表中向快取記憶體中載入資料時要做大量的保護性檢查,大致如下:
1. 段寄存器的值不能是0。根據規範,描述符表中的第一個描述符必須是空描述符,所以段寄存器值為0是不合法的。如果為0,產生異常中斷13.
2. 段寄存器中的值是否大於或等於描述符表的長度(存在GDTR中),如果大於或等於描述符表的長度,產生異常中斷13.
3. 如果段寄存器是CS,檢查描述符表中的段類型是否為程式碼片段,如果不是,產生異常中斷13.
4. 如果段寄存器CS要求裝入的段是程式碼片段,檢查描述符表該段是否存在,如果不存在產生異常中斷11
5. 如果段寄存器CS通過了3、4的檢查,還要檢查IP是否超越了該段的邊界,如果越界,產生異常中斷13
6. 如果段寄存器CS通過了3、4、5的檢查,則把相應的描述符裝入快取記憶體
7. 如果段寄存器不是CS,檢查描述符表中的段類型為資料段,如果不是產生異常中斷13
8. 如果段寄存器不是CS,該段為資料段,檢查其是否存在,如果不存在產生異常中斷12
9. 如果段寄存器不是CS,且通過了7、8檢查,則把相應的描述符裝入快取記憶體
以上過程,不一定很完整,大概就是這樣,從中,大家應該可以看出,所謂保護模式的保護方式,至少有一個感性認識。

說到這裡,基本上可以結束了,但是我們所說的描述符表實際上是非常膚淺的,實際上本文說到的描述符表叫做通用描述元表(Global Descriptor Table 簡稱GDT),還有局部描述符表,中斷描述符表等,但其原理大同小異,理解了GDT,其它的也就比較容易了
--> 閱讀更多...

2009年3月23日 星期一

●memtest86+教學 Part6



void do_test(void)
{
int i = 0, j = 0;
unsigned long chunks;
unsigned long lo, hi;

/* If we have a partial relocation finish it */
if (run_at_addr == (unsigned long)&_start) {
run_at_addr = 0xffffffff;
} else if (run_at_addr != 0xffffffff) {
__run_at(run_at_addr);
}

/* If first time, initialize test */
if (firsttime == 0) {
if ((ulong)&_start != LOW_TEST_ADR) {
restart();
}
init();
windows[0].start =
( LOW_TEST_ADR + (_end - _start) + 4095) shr 12;

/* Set relocation address at 16Mb if there is enough memory */
if (v->pmap[v->msegs-1].end gtr 0x1100) {
high_test_adr = 0x01000000;
}
windows[1].end = (high_test_adr shr 12);
firsttime = 1;
}
bail = 0;

//由於左移、右移、大於、小於的符號是html的標籤用途,因此以後程式碼若是有這些符號我便會用左移:shl 右移:shr 大於:gtr 小於:lss 的英文字符表示,否則顯示文章可能格式會有問題。
怎麼一進入c並沒有感到輕鬆自在;你就當作吃苦就是吃補。上面只是截取do_test前面一小段;或許你會每看一行code,心中就充滿一堆問題,真可說是如履薄冰,簡直就是破冰而行;對不起我喜歡有冰字的成語。 大家都已經知道do_test,我就不再多說,就直接進入這個routine:我們一小段一小段來吧! 因為windows[0]的值是(0,0x080000),因為它們是pmap所以單位是4k,所以0x80000對應到2G,但以圖PMAP-2,您會發現,經過compute_segments()routine求算出的結果:windows[0].start=0x21=132k,那是因為以下這一行: ( LOW_TEST_ADR + (_end - _start) + 4095) shr 12;你可以把它劃成下列等式8k+MT(120k)+4k=132K 這個MT為什麼會是120kㄋ,我自己編譯的memtest.bin目前是110k,所以不僅有10k差距,而且它MT只包含(_end-_start) ,並不包含boot code,因此差距就一定大於10k,所有我也不知道為什麼,反正目前就差不多就好了,而且你只要知道這 0→132k被memtest code給佔用了,所有平台都一樣,無法測得這部分。補充一點,就是640k到1mb這裡被dos系統給佔用了,所以也測不到。因此你會發現所有平台的第一個segs都一模一樣,你可參考圖PMAP-1及PMAP-2,都會是: (0X00000021,0X0000009F),即第一個SEGS【v->map】就是132K~636K的測試範圍。當然這是大約值,你可以代入mapping、emapping,就知道實際植,這裡就不囉唆了。也就是說0x00000021會隨著你的memtest86程式碼的大小來決定。若是看的相當吃力,沒關係,我們考慮從別的角度出發‧待續‧
不好意思,由於顯示問題,所以以上這段中文字都沒出現在畫面中;sorry!
--> 閱讀更多...

2009年3月22日 星期日

●memtest86+教學 Part5


我們知道在setup.S中所設定ㄉgdt有兩ㄍ描述子,一ㄍ是資料段一ㄍ是程式段,而且它們都只能定址到128mb,我們說明一下描述子好ㄌ:
我只說明那個G位元:0則段界限之Granularity(粒度)為byte,1則段界限之Granularity(粒度)為4Kbytes。因此來看一下setup.Sㄉ描述子:
.word 0x7FFF # limit 128mb 0x7fff*4k=128mb
.word 0x0000 # base address=0
.word 0x9A00 # code read/exec
.word 0x00C0 # granularity=4096, 386
其他部分請自行參考"自己動手寫作業系統"一書。
既然它進入保護模式,但僅設為可定址到128mb,那麼當它ljmp $KERNEL_CS, $(TSTLOAD <<4)之後,它勢必再重新設定一次gdt,所以你在head.s中又看到一個gdt表。接著就是設定idt,這部分以後再來談;接下來作一些CPU相關判斷,然後才是要進入C語言的殿堂。
leal _dl_start@GOTOFF(%ebx), %eax
call *%eax
call do_test
Reloc.c中ㄉvoid _dl_start(void),另一ㄍ是main.c的void do_test(void)

"void _dl_start(void)"其實這ㄍROUTINE很難懂,若你也看不懂,也可先跳過,只知道它和ELF檔案格式有關,因為我們用C寫ㄉ這些ROUTINEㄉEntry和符號表可能尚未全部定位好,所以才呼叫這個函式。因此這ㄍ函式與記憶體測試和架構無關,若你懂ㄉ話也請你教教我。
以下這ㄍ網址有關於這方面ㄉ知識:Before main()
void do_test(void)當然就是我門要談ㄉ重點ㄌ‧‧‧待續。
--> 閱讀更多...

2009年3月20日 星期五

●memtest86+教學 Part4


(爛豬腳‧‧續)_GLOBAL_OFFSET_TABLE_為基底位址的符號會
得到當前段的起始地址到global offset table的距離(offset),由於此
時已經是32bits的flat4G定址,因此引用這個符號相當等同於取得
.data segment及.bss區的啟始位址,因此我們比較一下為什麼setup.S
就不是用這種方式來計算offset值 ,因為那時還在真實模式,而現在
是在保護模式。
movl $(LOW_TEST_ADR + _GLOBAL_OFFSET_TABLE_), %esp
#LOW_TEST_ADR=0x2000=8k
所以這一行相當於,將堆疊指標暫存器設在.data區開始後的8k處。
/*********************************************************/
/* Load the GOT pointer */

call 0f        #為什要這樣call目的何在,目前I do'nt know.     
0: popl %ebx
addl $_GLOBAL_OFFSET_TABLE_+[.-0b], %ebx
上面這一行就是獲得標籤"0:",也就是popl %ebx這條指令的地址
,然後保存在%ebx中。
/*********************************************************/
/* Reload all of the segment registers */
leal gdt@GOTOFF(%ebx), %eax #取得gdt位址存入eax
movl %eax, 2 + gdt_descr@GOTOFF(%ebx)
#將gdtㄉ基底位址存到gdt_descr+2ㄉ位址
lgdt gdt_descr@GOTOFF(%ebx)
#看到這一行你就應該要知道上一行ㄉ目的 
leal flush@GOTOFF(%ebx), %eax
pushl $KERNEL_CS
pushl %eax
lret
#上面這三行是ㄍ很特殊ㄉ寫法,lret會pop出(cs)segment:offset
#所以相當於程式會繼續網下執行,但由於不能寫成如下:
#movl $KERNEL_CS, %eax
#movw %ax, %cs
#jmp flush 因為cs不可這樣搞

flush: movl $KERNEL_DS, %eax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss

/*
* Zero BSS 將_bss區清0
*/
cmpl $1, zerobss@GOTOFF(%ebx)
jnz zerobss_done
xorl %eax, %eax
leal _bss@GOTOFF(%ebx), %edi
leal _end@GOTOFF(%ebx), %ecx
subl %edi, %ecx
1: movl %eax, (%edi)
addl $4, %edi
subl $4, %ecx
jnz 1b
movl $0, zerobss@GOTOFF(%ebx)
zerobss_done:
此時你腦袋中應該要浮現出到目前為止程式在記
憶體中如何配置如上圖。
但這是沒有在dos環境下跑的流程:所以我們要找時間來看看這個mt86+_loader.asm檔,因此把它弄清楚就可以知道和正常的booting有何不同。

--> 閱讀更多...

●memtest86+教學 Part3

在我們進入最佳男主角(爛豬腳)ㄉ主題之前,我希望先把makefile及三ㄍlds檔 memtest.bin.lds、memtest.lds、memtest_shared.lds稍加探討探討。若你還不知道什麼是makefile那先去溫習功課再來看這裡。makefile的寫法你去比較一下1.70版,稍有不同,但達成ㄉ效果是一樣的。一般我們很少會去寫lds檔,因為寫apㄉ人搞不好根本就不知道這是什麼東東,lds檔是給linux下gcc附的linker連結器LD所參考用的,若你不指定lds檔,LD還是會叫用預設lds檔,故名思義就是連結器專用的script(描述檔),畢竟memtest86+不是OS下ㄉapplication它等於是ㄍ小型的OS,因為從Boot到run實際應用都是要去和實際硬體溝通,因此lds檔當然要符合它自己ㄉ需求。先截取部分makefile內容:
memtest.bin: memtest_shared.bin bootsect.o setup.o memtest.bin.lds
$(LD) -T memtest.bin.lds bootsect.o setup.o -b binary \
memtest_shared.bin -o memtest.bin
以上敘述: bootsect.o setup.o連結時要參考memtest.bin.lds
以下是
memtest.bin.ldsㄉ內容:
OUTPUT_FORMAT("binary")
OUTPUT_ARCH("i386")

ENTRY(_main);
SECTIONS {
. = 0;
.bootsect : { *(.bootsect) }
.setup : { *(.setup) }
.memtest : {
_start = . ;
*(.data)
_end = . ;
}
_syssize = (_end - _start + 15) >> 4;
}
若沒有全部看懂,先懂一半也行。
其他的makefile內容依此類推,應該難不倒大家。

爛豬腳:head.S
.code32
.globl startup_32
startup_32:
cld
cli
‧‧‧
看到沒,它是32位元的code,而且它沒有.section ㄉ宣告,因為它和Cㄉ部分是整合為同一區段,如下所示:
下面ㄉOBJS除了head.o是組合語言寫ㄉ,其餘都是C寫ㄉ,但這本來就無關緊要,重要ㄉ是這些物件檔被連結成
memtest_shared(依照memtest_shared.ldsㄉ敘述)
OBJS= head.o reloc.o main.o test.o init.o lib.o patn.o screen_buffer.o \
config.o linuxbios.o memsize.o pci.o controller.o random.o extra.o \
spd.o error.o dmi.o

memtest_shared: $(OBJS) memtest_shared.lds Makefile
$(LD) --warn-constructors --warn-common -static -T memtest_shared.lds \
-o $@ $(OBJS) && \
$(LD) -shared -Bsymbolic -T memtest_shared.lds -o $@ $(OBJS)

我們還是看一下
memtest_shared.ldsㄌㄌ等(台語)
OUTPUT_FORMAT("elf32-i386");
OUTPUT_ARCH(i386);

ENTRY(startup_32);
SECTIONS {
. = 0;
.text : {
_start = .;
*(.text)
*(.text.*)
*(.plt)
_etext = . ;
} = 0x9090
.rodata : {
*(.rodata)
*(.rodata.*)
}
.dynsym : { *(.dynsym) }
.dynstr : { *(.dynstr) }
.hash : { *(.hash) }
.dynamic : { *(.dynamic) }

.rel.text : { *(.rel.text .rel.text.*) }
.rel.rodata : { *(.rel.rodata .rel.rodata.*) }
.rel.data : { *(.rel.data .rel.data.*) }
.rel.got : { *(.rel.got .rel.got.*) }
.rel.plt : { *(.rel.plt .rel.plt.*) }

. = ALIGN(4);
.data : {
_data = .;
*(.data)
*(.data.*)
}
.got : {
*(.got.plt)
*(.got)
_edata = . ;
}
. = ALIGN(4);
.bss : {
_bss = .;
*(.dynbss)
*(.bss)
*(.bss.*)
*(COMMON)
/* _end must be at least 256 byte aligned */
. = ALIGN(256);
_end = .;
}
/DISCARD/ : { *(*) }
}

若是看ㄌ一頭霧水,至少你也看到startup_32,它就是head.Sㄉ頭,至於_start和 _end 請看:
memtest: memtest_shared.bin memtest.lds
$(LD) -s -T memtest.lds -b binary memtest_shared.bin -o $@
以下是 memtest.lds內容 0x10000=64k
OUTPUT_FORMAT("elf32-i386");
OUTPUT_ARCH(i386);

ENTRY(_start);
SECTIONS {
. = 0x10000;
_start = . ;
.data : {
*(.data)
}
}

所以startup_32=_startyou know!
--> 閱讀更多...

2009年3月19日 星期四

●桌面圖示陰影(背景)








這ㄍ問題解法相當簡單:




--> 閱讀更多...

●memtest86+教學 Part2


mt86+_loader.asm這ㄍ檔案就是我上一篇文章賣的關子,有ㄌ它你可以將memtest86+製作成在Dos底下run的執行檔:
exeh: db "MZ"
dw fullsize % 512 ; how much to load from
dw (fullsize + 511) / 512 ; .exe to RAM
dw 0 ; no relocations used
dw 2 ; header size is 2 * 16 bytes
dw stackpara ; minimum heap is 128 * 16 bytes, for stack
dw stackpara ; we do not need more heap either
dw (fullsize + 15) / 16 ; SS is after file
; segment offsets are relative to PSPseg+10h
; initial DS and ES point to PSPseg, and file
; except headers is loaded to PSPseg+10h.

dw stacksize-4 ; initial SP value
dw 0 ; no checksum
dw 100h ; initial IP
dw -10h ; initial CS relative to PSPseg+10h
dw 0 ; no relocation table, "offset 0 in file"
dw 0 ; this is not an overlay
db "MEMT" ; padding to a multiple of 16 bytes
以上這些資訊是從mt86+_loader.asm截取下來,若你看不明白可以參考這份資訊,其他的看完有問題再說。因為到目前為止都還沒進入主題,所以先進入memtest86+殿堂看看究竟‧‧‧。
註:本文使用版本是memtest86+-2.01;首先看一下上圖有哪些檔案:不好意思好像不是頂清楚。首先我們看我圈選起來的那幾個檔案,先說明main.c吧!不要被檔案名稱給誤導ㄌ,學Cㄉ時候是要你知道main這ㄍroutine而不是檔名,所以這ㄍ檔案根本就不是程式ㄉentry(進入點)所在。
另外三ㄍ副檔名為大寫"S"的檔案bootsect.S、head.S、setup.S才是主角(注意,不要在windows將memtest86+-2.01.tar.gz解壓縮,因為windows不區分檔名大小寫),我並不想針對這三ㄍ檔案涉入太深,否則一樣會廢話一堆,但玩過Linux Kernelㄉ大大們對這三個檔案一定不陌生,網路上也有粉多這方面ㄉ討論,但是這三個檔案到底還是很重要,若你完全看不懂,我想你可能要花一段不算短ㄉ時間將它搞懂,因為你若不懂,則接下來的C語言你會Trace的很吃力,也不踏實,雖然說網路上有針對這三ㄍ檔案ㄉ論述,但畢竟memtest86已經修改過因此我會稍為作說明:
這三個檔案是GAS的語法:你必須自己搞懂GAS(Assembly),但mt86+_loader.asm又是nasm的語法‧‧‧‧這些主題若是我精力過旺,或許以後再另闢文章。
1.bootsect.S:先把自己512Bytes從0x7c00搬移到0x90000,並繼續執行;繼續執行的第一ㄍ動做就是movw %cs, %ax #讓cs值也等於0x9000,接著處理一些磁碟參數問題後將setup.S從1.44mbㄉ磁片讀到0x92000,然後是# we want to load the system (at 0x10000):將整ㄍ程式主體從磁碟讀到0x10000處,這個動作就是call read_it,並從0x90200處開始執行,這ㄍ位址就是setup.Sㄉ啟始處。所以bootsect.S所完成的工作就是將程式從磁碟讀到記憶體後再接著去執行setup.S。
所以若你不作成dos版本ㄉ執行檔,原則上執行ㄉ流程是從bootsect.S開始,但相反ㄉ若作成dos版本ㄉ執行檔則bootsect.S幾乎算是ㄍ廢物,因為mt86+_loader執行完就會跳到setup.S
2.setup.S:這部份就是準備進入保護模式ㄉ一些動作,但它不搞cr0,它用lmsw這ㄍ指令進保護模式,反正道理都一樣,若你有認真去看完setup.S,你會發現它只是單純ㄉ進入保護模式,但這樣ㄉ話也可以在head.S作啊,其實說穿ㄌ它用這ㄍ檔主要是啟用a20位址線,由於這部份方法實作有三個,而它(setup.S)通通作,為什麼?請看我postㄉ文章"Fast A20 和92H,及8042到底是什麼關係";其他(改天再詳述)。
3.head.S:這ㄍ檔案才是最佳男主角‧‧‧待續。
--> 閱讀更多...

●memtest86+教學 Part1


首先廢話一篇:我一直很渴望能從事bios相關行業,因為我確定很多你我不了解的規範specification是如何實作他們最清楚,即使從事那一行沒有在ami或award、phoenix這種外商大公司,一樣可以學習到很多東西。也因此我的學習格外牛步;但憑藉"新聞挖挖哇"的精神,從一套看似不起眼的軟體,一樣可以精進你的專業,學習memtest86+就等於是在學習如何玩弄記憶體,然而這也是跟底層很近的起點,畢竟資訊軟體業說破ㄌ就真的是在玩弄記憶體,只是玩法不同便衍生出不同的技術、演算法‧‧‧‧‧真不好意思,用"玩弄"兩ㄍ字對自己好像也不是很敬業。
言歸正傳,memtest86+及memtest86有什麼不同,memtest86+發行版本是base on memtest86,並作些修正因此release的版本較慢吧!關於作者Memtest86+ is written by Samuel Demeulemeester ,the original author of memtest86 is Chris Brady 。但"+"就是有加入一些東西ㄉ意思。加入ㄌ什麼,先賣ㄍ關子請往下看‧‧
首先你必須具備什麼技能: 1會使用linux(不用很厲害,只要你可以安裝完成linux,其他慢慢來.)2你要會看懂C及Assembly.3對X86架構要了解(這部份要時間ㄉ累積,以及你棄而不捨ㄉ精神,誰叫你我都是門外漢又找不到高高手來做朋友)。我個人是用Microsoft virtual PC或Sun xVM VirtualBox安裝linux,然後掛起samba sever,在windows用source Insight來編輯code,這樣很方便,也是我ㄍ人的習慣,畢竟windows有些好用的tools,但你若是linux高手就當我是在廢話。以上的畫面是我ㄉvirtual PC 啟動ubuntu linux的畫面。
--> 閱讀更多...

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

以下是從DOS编程技术這ㄍ大陸博客寫ㄉ資料放ㄌ上來,它裡面還有介紹一些好東西。您可以慢慢參考、品嘗。
早期的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, 92h
in al, dx
如果al的bit 1為1表示a20打開,否則為0
• 打開A20
mov dx, 92h
mov al, 02
out dx, al
• 關閉A20
mov dx, 92h
mov al, 0
out 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以外的定義是什麼我至今也不知道。

更多參考:Gate A20與保護模式
--> 閱讀更多...

2009年3月18日 星期三

●8042 Keyboard Controller

IO Port 60h為鍵盤的輸入暫存器 、61h為控制暫存器、64h狀態暫存器
8042 - Keyboard Controller
8042 Status Register (port 64h read)
8042 Status Register
Bit0|------output register (60h) has data for system
Bit1|------input register (60h/64h) has data for 8042
Bit2|------system flag (set to 0 after power on reset)
Bit3|------data in input register is command (1) or data (0)
Bit4|------1=keyboard enabled, 0=keyboard disabled (via switch)
Bit5|------1=transmit timeout (data transmit not complete)
Bit6|------1=receive timeout (data transmit not complete)
Bit7|------1=even parity rec'd, 0=odd parity rec'd (should be odd)

Port Mode Description

64h read 8042 status register. Can be read at any time. See table above for more information.
64h write 8042 command register. Writing this port sets Bit3 of the status register to 1 and the byte is treated as a controller command. Devices attached to the 8042 should be disabled before issuing commands that return data since data in the output register will be overwritten.
60h read 8042 output register (should only be read if Bit 0 of status port is set to 1)
60h write 8042 data register. Data should only be written if Bit 1 of the status register is zero (register is empty).
When this port is written Bit 3 of the status register is set to zero and the byte is treated as a data. The 8042 uses this byte if it's expecting data for a previous command, otherwise the data is written directly to the keyboard. See ~KEYBOARD COMMANDS~ for information on
programming the actual keyboard hardware.

8042 Commands Related to PC Systems (Port 64h)

Command Description

20 Read 8042 Command Byte: current 8042 command byte is placed in port 60h.
60 Write 8042 Command Byte: next data byte written to port 60h is placed in 8042 command register. Format:

8042 Command Byte
Bit0|------1=enable output register full interrupt
Bit1|------ should be 0
Bit2|------1=set status register system, 0=clear
Bit3|------ 1=override keyboard inhibit, 0=allow inhibit
Bit4|------disable keyboard I/O by driving clock line low
Bit5|------ disable auxiliary device, drives clock line low
Bit6|------IBM scancode translation 0=AT, 1=PC/XT
Bit7|------reserved, should be 0

A4 Password Installed Test: returned data can be read from port 60h; FA=password installed, F1=no password
A5 Load Security: bytes written to port 60h will be read until a null (0) is found.
A6 Enable Security: works only if a password is already loaded
A7 Disable Auxiliary Interface: sets Bit 5 of command register stopping auxiliary I/O by driving the clock line low
A8 Enable Auxiliary Interface: clears Bit 5 of command register
A9 Auxiliary Interface Test: clock and data lines are tested; results placed at port 60h are listed below:

00 no error
01 keyboard clock line is stuck low
02 keyboard clock line is stuck high
03 keyboard data line is stuck low
04 keyboard data line is stuck high

AA Self Test: diagnostic result placed at port 60h, 55h=OK
AB Keyboard Interface Test: clock and data lines are tested; results placed at port 60h are listed above with command A9
AC Diagnostic Dump: sends 16 bytes of 8042's RAM, current input port state, current output port state and 8042 program status
word to port 60h in scan-code format.
AD Disable Keyboard Interface: sets Bit 4 of command register stopping keyboard I/O by driving the clock line low
AE Enable Keyboard Interface: clears Bit 4 of command register enabling keyboard interface.
C0 Read Input Port: data is read from its input port (which is inaccessible to the data bus) and written to output register at port 60h; output register should be empty before call.

8042 Input Port
Bit0-3|------undefined
Bit4|------ 1=enable 2nd 256K of motherboard RAM, 0=disable
Bit5|------1=manufacturing jumper not installed, 0=installed
Bit6|------ 1=primary display is MDA, 0=primary display is CGA
Bit7|------1=keyboard not inhibited, 0=keyboard inhibited

C1 Poll Input Port Low Bits: Bits 0-3 of port 1 placed in status Bits 4-7
C2 Poll Input Port High Bits: Bits 4-7 of port 1 placed in status Bits 4-7
D0 Read Output Port: data is read from 8042 output port (which is inaccessible to the data bus) and placed in output register;
the output register should be empty. (see command D1 below)
D1 Write Output Port: next byte written to port 60h is placed in the 8042 output port (which is inaccessible to the data bus)

8042 Output Port
Bit0|------system reset line
Bit1|------gate A20
Bit2-3|------undefined
Bit4|------ output buffer full
Bit5|------input buffer empty
Bit6|------keyboard clock (output)
Bit7|------keyboard data (output)

D2 Write Keyboard Output Register: on PS/2 systems the next data
byte written to port 60h input register is written to port 60h
output register as if initiated by a device; invokes interrupt
if enabled
D3 Write Auxiliary Output Register: on PS/2 systems the next data
byte written to port 60h input register is written to port 60h
output register as if initiated by a device; invokes interrupt
if enabled
D4 Write Auxiliary Device: on PS/2 systems the next data byte
written to input register a port at 60h is sent to the
auxiliary device
E0 Read Test Inputs: 8042 reads its T0 and T1 inputs; data is
placed in output register; Bit 0 is T0, Bit 1 is T1:

Test Input Port Bits
Bit0|------keyboard clock
Bit1|------keyboard data

Fx Pulse Output Port: Bits 0-3 of the 8042 output port can be pulsed low for 6 us(微秒);Bits 0-3 of command indicate which Bits should be pulsed; 0=pulse, 1=don't pulse; pulsing
Bit 0 results in CPU reset since it is connected to system reset line.
--> 閱讀更多...

●讀網卡MAC Address有這麼難嗎?


或許你知道ipconfig,linux:ifconfig;但是很抱歉,我不想在有os的狀況讀出這些資訊,更不想拆下eeprom直接讀取IC。說穿了就是我現在工作遇上的瀕頸(不知這兩ㄍ字有沒有打錯,好久沒用)因為我知道bios有提供int 1ah中斷,從這部分我們可以去找出你想要的pci device的PFA,再從PFA中透過bus、device、function去找出真正你想找到的device,再從device中的BAR(Base Address Register)中找出你要的io port這樣就可以從port讀出你要的mac address。很幸運也很不幸rtl8139、rtl8168讀出來都正確無誤,不管是內建網卡還是外插螃蟹卡都可以,但是問題來ㄌ,Marvell Yukon的網卡卻讓我失去信心,原因是它是memory mappied而不是I/O mappied,還是說它是PCI-e的Device,不好意思,我還不確定,若你是高手高高手,煩請指點迷津。‧‧‧‧‧‧迷途羔羊--------Benson,也有人稱呼我老B,雖然這兩ㄍ字有點不雅。


這個問題終於搞定,感謝jajamboliao的熱心指導。現在可以放下它ㄌ,若沒有高人指點,上面那張圖我已經跑了3個loop,結果每次放下它,都是因為處理不了它;now;如釋重負。
--> 閱讀更多...

●Borland C++3.1 Compile時無法辨認內嵌組合語言的32bit register




這部分ㄉ解決辦法就是要設定兩ㄍ地方: 1.Options/Compiler/Code generation/Compile via assembler 2.Options/Compiler/Advanced code generation/80386 搞定。
--> 閱讀更多...

●learnning assembly

Iczelion's Win32 Assembly Homepage
小木偶

組合語言程式設計

還有很多‧‧‧‧但是希望我們都是一步一腳印的人。
--> 閱讀更多...

●中斷、保護模式、分頁‧‧真是夠ㄌ


以前寫AP知道有保護模式這玩意,但壓根想也想不到我也要碰這一塊;若你想往下看,我想你ㄉ功力一定深不見底,若你對這些和我以前一樣還搞的很模糊,那建議你去買本書先:"自己動手寫作業系統";于淵編著。把它的第三章搞懂你就成大功立大業ㄌ。當然若你是大內高手,每天都和這些玩意兒打混,那這裡你可以來點建議吧!
以下資料是大陸網站找到的,可以先暖暖身子:但因為資料太多,所以真正ㄉ保護模式都還沒有進入主題,先SAY SORRY,下回就會有●中斷、保護模式、分頁‧‧真是夠ㄌ___PART2,請奈心等待‧‧‧
初級接觸ASM者學習資料(1)
組合語言和CPU以及記憶體,埠等硬體知識是連在一起的. 這也是為什麼組合語言沒有通用性的原因.
下面簡單講講基本知識(針對INTEL x86及其兼容機) ============================ x86組合語言的指令,其操作物件是CPU上的寄存器,系統記憶體,或者立即數. 有些指令表面上沒有運算元, 或者看上去缺少運算元, 其實該指令有內定的操作物件, 比如push指令, 一定是對SS:ESP指定的記憶體操作, 而cdq的操作物件一定是eax / edx. 在組合語言中,寄存器用名字來訪問. CPU 寄存器有好幾類, 分別有不同的用處: 1. 通用寄存器: EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP(這個雖然通用,但很少被用做除了堆疊指標外的用途) 這些32位可以被用作多種用途,但每一個都有"專長". EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器. EBX 是"基底位址"(base)寄存器, 在記憶體定址時存放基底位址. ECX 是計數器(counter), 是重複(REP)首碼指令和LOOP指令的內定計數器. EDX是...(忘了..哈哈)但它總是被用來放整數除法產生的餘數. 這4個寄存器的低16位可以被單獨訪問,分別用AX,BX,CX和DX. AX又可以單獨訪問低8位(AL)和高8位(AH), BX,CX,DX也類似. 函數的返回值經常被放在EAX中. ESI/EDI分別叫做"源/目標索引寄存器"(source/destination index),因為在很多字串操作指令中, DS:ESI指向源串,而ES:EDI指向目標串. EBP是"基址指標"(BASE POINTER), 它最經常被用作高階語言函數調用的"框架指標"(frame pointer). 在破解的時候,經常可以看見一個標準的函數起始代 碼: push ebp ;保存當前ebp mov ebp,esp ;EBP設為當前堆疊指標 sub esp, xxx ;預留xxx位元組給函數臨時變數....
這樣一來,EBP 構成了該函數的一個框架, 在EBP上方分別是原來的EBP, 返回地址和參數. EBP下方則是臨時變數. 函數 返回時作 mov esp,ebp/pop ebp/ret 即可. ESP 專門用作堆疊指標. 2. 段寄存器: CS(Code Segment,代碼段) 指定 當前執行的代碼段. EIP (Instruction pointer, 指令指標)則指向該段中一個具體的指令. CS:EIP指向哪個指令, CPU 就執行它. 一般只能用jmp, ret, jnz, call 等指令來改變程式流程,而不能直接對它們賦值. DS(DATA SEGMENT, 資料段) 指定一個資料段. 注意:在當前的電腦系統中, 代碼和資料沒有本質差別, 都是一串二進位數字, 區別只在於 你如何用它. 例如, CS 制定的段總是被用作代碼, 一般不能通過CS指定的地址去修改該段. 然而,你可以為同一個段 申請一個資料段描述符"別名"而通過DS來訪問/修改. 自修改代碼的程式常如此做. ES,FS,GS 是輔助的段寄存器, 指定 附加的資料段. SS(STACK SEGMENT)指定當前堆疊段. ESP 則指出該段中當前的堆疊頂. 所有push/pop 系列指令都只對SS:ESP指出的位址進行操作. 3. 標誌寄存器(EFLAGS): 該暫存器有32位元,組合了各個系統標誌. EFLAGS一般不作為整 體訪問, 而只對單一的標誌位元感興趣. 常用的標誌有: 進位元標誌C(CARRY), 在加法產生進位或減法有借位時置1, 否 則為0. 零標誌Z(ZERO), 若運算結果為0則置1, 否則為0 符號位元S(SIGN), 若運算結果的最高位置1, 則該位也置1. 溢 出標誌O(OVERFLOW), 若(帶符號)運算結果超出可表示範圍, 則置1. JXX 系列指令就是根據這些標誌來決定是否要跳轉,
從而實現條件分枝. 要注意,很多JXX 指令是等價的, 對應相同的機器碼. 例如, JE 和JZ 是一樣的,都是當Z=1是跳轉.只有JMP 是無條件跳轉. JXX 指令分為兩組, 分別用於無符號操作和帶符號操作. JXX 後面的"XX" 有如下字母: 無符號 操作: 帶符號操作: A = "ABOVE", 表示"高於" G = "GREATER", 表示"大於" B = ELOW", 表示"低於" L = "LESS", 表 示"小於" C = "CARRY", 表示"進位"或"借位" O = "OVERFLOW", 表示"溢出" S = "SIGN", 表示"負" 通用符號: E = "EQUAL" 表示"等於", 等價於Z (ZERO) N = "NOT" 表示"非", 即標誌沒有置位元. 如JNZ "如果Z沒有置位則跳轉" Z = "ZERO", 與E同. 如果仔細想一想,就會發現 JA = JNBE, JAE = JNB, JBE = JNA, JG = JNLE, JGE= JNL, JL= JNGE, .... 4. 埠埠是直接和外部設備通訊的地方。外設接入系統後,系統就會把外設的資料介面映射到特定的埠位址空間,這樣,從該埠讀入資料就是 從外設讀入資料,而向外設寫入資料就是向埠寫入資料。當然這一切都必須遵循外設的工作方式。埠的位址空間與記憶體位 址空間無關,系統總共提供對64K個8位埠的訪問,編號 0-65535. 相鄰的8位埠可以組成成一個16位埠,相鄰的16位埠可以組 成一個32位埠。埠輸入輸出由指令IN,OUT,INS和OUTS實現,具體可參考組合語言書籍。
初級接觸ASM者學習資料(2)

ASM指令的運算元可以是記憶體中的資料, 如何讓程式從記憶體中正確取得所需要的資料就是對記憶體的定址.
INTEL 的CPU 可以工作在兩種定址模式:實模式和保護模式. 前者已經過時,就不講了, WINDOWS 現在是32位元保護模式的系統, PE 檔就基本是運行在一個32位元線性位址空間, 所以這裏就只介紹32位元線性空間的定址方式.
其實線性位址的概念是很直觀的, 就想像一系列位元組排成一長隊,第一個位元組編號為0, 第二個編號位1, .... 一直到4294967295(十六進位FFFFFFFF,這是32位二進位數字所能表達的最大值了). 這已經有4GB的容量! 足夠容納一個程式所有的代碼和資料. 當然, 這並不表示你的機器有那麼多記憶體. 實體記憶體的管理和分配是很複雜的內容, 初學者不必在意, 總之, 從程式本身的角度看, 就好象是在那麼大的記憶體中.
在INTEL系統中, 記憶體位址總是由"段選擇符:有效位址"的方式給出.段選擇符(SELECTOR)存放在某一個段寄存器中, 有效位址則可由不同的方式給出. 段選擇符通過檢索段描述符確定段的起始位址, 長度(又稱段限制), 粒度, 存取許可權, 訪問性質等. 先不用深究這些, 只要知道段選擇符可以確定段的性質就行了. 一旦由選擇符確定了段, 有效位址相對于段的基底位址開始算. 比如由選擇符1A7選擇的資料段, 其基底位址是400000, 把1A7 裝入DS中, 就確定使用該資料段. DS:0 就指向線性位址400000. DS:1F5278 就指向線性位址5E5278. 我們在一般情況下, 看不到也不需要看到段的起始位址, 只需要關心在該段中的有效位址就行了. 在32位元系統中, 有效位址也是由32位元數字表示, 就是說, 只要有一個段就足以涵蓋4GB線性位址空間, 為什麼還要有不同的段選擇符呢? 正如前面所說的, 這是為了對資料進行不同性質的訪問. 非法的訪問將產生異常中斷, 而這正是保護模式的核心內容, 是構造優先順序和多工系統的基礎. 這裏有涉及到很多深層的東西, 初學者先可不必理會.
有效位址的計算方式是: 基址+間址*比例因數+偏移量. 這些量都是指段內的相對于段起始位址的量度, 和段的起始位址沒有關係. 比如, 基址=100000, 間址=400, 比例因數=4, 偏移量=20000, 則有效位址為:
100000+400*4+20000=100000+1000+20000=121000. 對應的線性位址是400000+121000=521000. (注意, 都是十六進位數).
基址可以放在任何32位通用寄存器中, 間址也可以放在除ESP外的任何一個通用寄存器中. 比例因數可以是1, 2, 4 或8. 偏移量是立即數. 如: [EBP+EDX*8+200]就是一個有效的有效位址運算式. 當然, 多數情況下用不著這麼複雜, 間址,比例因數和偏移量不一定要出現.
記憶體的基本單位是位元組(BYTE). 每個位元組是8個二進位位元, 所以每個位元組能表示的最大的數是11111111, 即十進位的255. 一般來說, 用十六進位比較方便, 因為每4個二進位位元剛好等於1個十六進位位, 11111111b = 0xFF. 記憶體中的位元組是連續存放的, 兩個位元組構成一個字(WORD), 兩個字構成一個雙字(DWORD). 在INTEL架構中, 採用small endian格式, 即在記憶體中,高位位元組在低位元位元組後面. 舉例說明:十六進位數803E7D0C, 每兩位元是一個位元組, 在記憶體中的形式是: 0C 7D 3E 80. 在32位寄存器中則是正常形式,如在EAX就是803E7D0C. 當我們的形式位址指向這個數的時候,實際上是指向第一個位元組,即0C. 我們可以指定訪問長度是位元組, 字或者雙字. 假設DS:[EDX]指向第一個位元組0C:
mov AL, byte ptr DS:[EDX] ;把位元組0C存入AL
mov AX, word ptr DS:[EDX] ;把字7D0C存入AX
mov EAX, dword ptr DS:[EDX] ;把雙字803E7D0C存入EAX
在段的屬性中,有一個就是缺省訪問寬度.如果缺省訪問寬度為雙字(在32位元系統中經常如此),那麼要進行位元組或字的訪問,就必須用byte/word ptr顯式地指明.
缺省段選擇:如果指令中只有作為段內偏移的有效位址,而沒有指明在哪一個段裏的時候,有如下規則:
如果用ebp和esp作為基址或間址,則認為是在SS確定的段中;
其他情況,都認為是在DS確定的段中。
如果想打破這個規則,就必須使用段超越首碼。舉例如下:
mov eax, dword ptr [edx] ;缺省使用DS,把DS:[EDX]指向的雙字送入eax
mov ebx, dword ptr ES:[EDX] ;使用ES:段超越首碼,把ES:[EDX]指向的雙字送入ebx
堆疊:
堆疊是一種資料結構,嚴格地應該叫做“棧”。“堆”是另一種類似但不同的結構。SS 和 ESP 是INTEL對棧這種資料結構的硬體支援。push/pop指令是專門針對棧結構的特定操作。SS指定一個段為棧段,ESP則指出當前的棧頂。push xxx 指令作如下操作:
把ESP的值減去4;
把xxx存入SS:[ESP]指向的記憶體單元。
這樣,esp的值減小了4,並且 SS:[ESP]指向新壓入的xxx. 所以棧是“倒著長”的,從高位址向低位址方向擴展。pop yyy 指令做相反的操作,把SS:[ESP]指向的雙字送到yyy指定的寄存器或記憶體單元,然後把esp的值加上4。這時,認為該值已被彈出,不再在棧上了,因為它雖然還暫時存在在原來的棧頂位置,但下一個push操作就會把它覆蓋。因此,在棧段中位址低於esp的記憶體單元中的資料均被認為是未定義的。
最後,有一個要注意的事實是,組合語言是面向機器的,指令和機器碼基本上是一一對應的,所以它們的實現取決於硬體.有些看似合理的指令實際上是不存在的,比如:
mov DS:[edx], ds:[ecx] ;記憶體單元之間不能直接傳送
mov DS, 1A7 ;段寄存器不能直接由立即數賦值
mov EIP, 3D4E7 ;不能對指令指標直接操作.
初級接觸ASM者學習資料(3)

“ 組合語言”作為一門語言,對應於高階語言的編譯器,我們需要一個“ASM器”來把組合語言原文件ASM成機器可執行的代碼。高級的ASM器如MASM, TASM等等為我們寫組合語言程式提供了很多類似於高階語言的特徵,比如結構化、抽象等。在這樣的環境中編寫的組合語言程式,有很大一部分是面向ASM器的虛擬指令,已經類同於高階語言。現在的ASM環境已經如此高級,即使全部用組合語言來編寫windows的應用程式也是可行的,但這不是組合語言的長處。組合語言的長處在於編寫高效且需要對機器硬體精確控制的程式。而且我想這裏的人學習ASM的目的多半是為了在破解時看懂反編譯代碼,很少有人真的要拿組合語言編程式吧?(汗......)

好了,言歸正傳。大多數組合語言書都是面向組合語言編程的,我的帖是面向機器和反編譯的,希望能起到相輔相成的作用。有了前面兩篇的基礎,組合語言書上對大多數指令的介紹應該能夠看懂、理解了。這裏再講一講一些常見而操作比較複雜的指令。我這裏講的都是機器的硬指令,不針對任何ASM器。

無條件轉移指令jmp:

這種跳轉指令有三種方式:短(short),近(near)和遠(far)。短是指要跳至的目標位址與當前位址前後相差不超過128位元組。近是指跳轉的目標位址與當前位址在用一個段內,即CS的值不變,只改變EIP的值。遠指跳到另一個代碼段去執行,CS/EIP都要改變。短和近在編碼上有所不同,在ASM指令中一般很少顯式指定,只要寫 jmp 目標位址,幾乎任何ASM器都會根據目標位址的距離採用適當的編碼。遠轉移在32位元系統中很少見到,原因前面已經講過,由於有足夠的線性空間,一個程式很少需要兩個代碼段,就連用到的系統模組也被映射到同一個位址空間。

jmp的運算元自然是目標位址,這個指令支援直接定址和間接定址。間接定址又可分為寄存器間接定址和記憶體間接定址。舉例如下(32位元系統):

jmp 8E347D60 ;直接定址段內跳轉
jmp EBX ;寄存器間接定址:只能段內跳轉
jmp dword ptr [EBX] ;記憶體間接定址,段內跳轉
jmp dword ptr [00903DEC] ;同上
jmp fward ptr [00903DF0] ;記憶體間接定址,段間跳轉

解釋:
在32位元系統中,完整目標位址由16位元段選擇子和32位偏移量組成。因為寄存器的寬度是32位,因此寄存器間接定址只能給出32位偏移量,所以只能是段內近轉移。在記憶體間接定址時,指令後面是方括號內的有效位址,在這個位址上存放跳轉的目標位址。比如,在[00903DEC]處有如下資料:7C 82 59 00 A7 01 85 65 9F 01

記憶體位元組是連續存放的,如何確定取多少作為目標位址呢?dword ptr 指明該有效位址指明的是雙字,所以取
0059827C作段內跳轉。反之,fward ptr 指明後面的有效位址是指向48位完全位址,所以取19F:658501A7 做遠跳轉。

注意:在保護模式下,如果段間轉移涉及優先順序的變化,則有一系列複雜的保護檢查,現在可不加理會。將來等各位功力提升以後可以自己去學習。

條件轉移指令jxx:只能作段內轉移,且只支援直接定址。

=========================================
調用指令CALL:

Call的定址方式與jmp基本相同,但為了從副程式返回,該指令在跳轉以前會把緊接著它的下一條指令的位址壓進堆疊。如果是段內調用(目標位址是32位元偏移量),則壓入的也只是一個偏移量。如果是段間調用(目標位址是48位元全位址),則也壓入下一條指令的完全位址。同樣,如果段間轉移涉及優先順序的變化,則有一系列複雜的保護檢查。

與之對應retn/retf指令則從副程式返回。它從堆疊上取得返回位址(是call指令壓進去的)並跳到該位址執行。retn取32位偏移量作段內返回,retf取48位全地址作段間返回。retn/f 還可以跟一個立即數作為運算元,該數實際上是從堆疊上傳給副程式的參數的個數(以字計)返回後自動把堆疊指標esp加上指定的數*2,從而丟棄堆疊中的參數。這裏具體的細節留待下一篇講述。

雖然call和ret設計為一起工作,但它們之間沒有必然的聯繫。就是說,如果你直接用push指令向堆疊中壓入一個數,然後執行ret,他同樣會把你壓入的數作為返回位址,而跳到那裏去執行。這種非正常的流程轉移可以被用作反跟蹤手段。
==========================================
中斷指令INT n

在保護模式下,這個指令必定會被作業系統截獲。在一般的PE程式中,這個指令已經不太見到了,而在DOS時代,中斷是調用作業系統和BIOS的重要途徑。現在的程式可以文質彬彬地用名字來調用windows功能,如 call user32!getwindowtexta。從程式角度看,INT指令把當前的標誌寄存器先壓入堆疊,然後把下一條指令的完全位址也壓入堆疊,最後根據運算元n來檢索“中斷描述符表”,試圖轉移到相應的中斷服務程式去執行。通常,中斷服務程式都是作業系統的核心代碼,必然會涉及到優先順序轉換和保護性檢查、堆疊切換等等,細節可以看一些高級的教程。

與之相應的中斷返回指令IRET做相反的操作。它從堆疊上取得返回位址,並用來設定CS:EIP,然後從堆疊中彈出標誌寄存器。注意,堆疊上的標誌寄存器值可能已經被中斷服務程式所改變,通常是進位元標誌C, 用來表示功能是否正常完成。同樣的,IRET也不一定非要和INT指令對應,你可以自己在堆疊上壓入標誌和位址,然後執行IRET來實現流程轉移。實際上,多工作業系統常用此伎倆來實現任務轉換。

廣義的中斷是一個很大的話題,有興趣可以去查閱系統設計的書籍。

============================================
裝入全指標指令LDS,LES,LFS,LGS,LSS

這些指令有兩個運算元。第一個是一個通用寄存器,第二個運算元是一個有效位址。指令從該位址取得48位全指標,將選擇符裝入相應的段寄存器,而將32位偏移量裝入指定的通用寄存器。注意在記憶體中,指標的存放形式總是32位偏移量在前面,16位選擇符在後面。裝入指標以後,就可以用DS:[ESI]這樣的形式來訪問指標指向的資料了。

============================================
字串操作指令

這裏包括CMPS,SCAS,LODS,STOS,MOVS,INS和OUTS等。這些指令有一個共同的特點,就是沒有顯式的運算元,而由硬體規定使用DS:[ESI]指向源字串,用ES:[EDI]指向目的字串,用AL/AX/EAX做暫存。這是硬體規定的,所以在使用這些指令之前一定要設好相應的指針。
這裏每一個指令都有3種寬度形式,如CMPSB(位元組比較)、CMPSW(字比較)、CMPSD(雙字比較)等。
CMPSB:比較源字串和目標字串的第一個字元。若相等則Z標誌置1。若不等則Z標誌置0。指令執行完後,ESI 和EDI都自動加1,指向源/目標串的下一個字元。如果用CMPSW,則比較一個字,ESI/EDI自動加2以指向下一個字。
如果用CMPSD,則比較一個雙字,ESI/EDI自動加4以指向下一個雙字。(在這一點上這些指令都一樣,不再贅述)
SCAB/W/D 把AL/AX/EAX中的數值與目標串中的一個字元/字/雙字比較。
LODSB/W/D 把源字串中的一個字元/字/雙字送入AL/AX/EAX
STOSB/W/D 把AL/AX/EAX中的直送入目標字串中
MOVSB/W/D 把源字串中的字元/字/雙字複製到目標字串
INSB/W/D 從指定的埠讀入字元/字/雙字到目標字串中,埠號碼由DX寄存器指定。
OUTSB/W/D 把源字串中的字元/字/雙字送到指定的埠,埠號碼由DX寄存器指定。

串操作指令經常和重複首碼REP和迴圈指令LOOP結合使用以完成對整個字串的操作。而REP首碼和LOOP指令都有硬體規定用ECX做迴圈計數器。舉例:

LDS ESI,SRC_STR_PTR
LES EDI,DST_STR_PTR
MOV ECX,200
REP MOVSD

上面的代碼從SRC_STR拷貝200個雙字到DST_STR. 細節是:REP首碼先檢查ECX是否為0,若否則執行一次MOVSD,ECX自動減1,然後執行第二輪檢查、執行......直到發現ECX=0便不再執行MOVSD,結束重複而執行下麵的指令。


LDS ESI,SRC_STR_PTR
MOV ECX,100
LOOP1:
LODSW
.... (deal with value in AX)

LOOP LOOP1
.....

從SRC_STR處理100個字。同樣,LOOP指令先判斷ECX是否為零,來決定是否迴圈。每迴圈一輪ECX自動減1。

REP和LOOP 都可以加上條件,變成REPZ/REPNZ 和 LOOPZ/LOOPNZ. 這是除了ECX外,還用檢查零標誌Z. REPZ 和LOOPZ在Z為1時繼續迴圈,否則退出迴圈,即使ECX不為0。REPNZ/LOOPNZ則相反。

初級接觸ASM者學習資料(4)

高階語言程式的ASM解析

在高階語言中,如C和PASCAL等等,我們不再直接對硬體資源進行操作,而是面向於問題的解決,這主要體現在資料抽象化和程式的結構化。例如我們用變數名來存取資料,而不再關心這個資料究竟在記憶體的什麼地方。這樣,對硬體資源的使用方式完全交給了編譯器去處理。不過,一些基本的規則還是存在的,而且大多數編譯器都遵循一些規範,這使得我們在閱讀反編譯代碼的時候日子好過一點。這裏主要講講ASM代碼中一些和高階語言對應的地方。

1. 普通變數。通常聲明的變數是存放在記憶體中的。編譯器把變數名和一個記憶體位址聯繫起來(這裏要注意的是,所謂的“確定的位址”是對編譯器而言在編譯階段算出的一個臨時的位址。在連接成可執行檔並載入到記憶體中執行的時候要進行重定位等一系列調整,才生成一個即時的記憶體位址,不過這並不影響程式的邏輯,所以先不必太在意這些細節,只要知道所有的函數名字和變數名字都對應一個記憶體的位址就行了),所以變數名在ASM代碼中就表現為一個有效位址,就是放在方括號中的運算元。例如,在C檔中聲明:

int my_age;

這個整型的變數就存在一個特定的記憶體位置。語句 my_age= 32; 在反ASM代碼中可能表現為:

mov word ptr [007E85DA], 20

所以在方括號中的有效位址對應的是變數名。又如:

char my_name[11] = "lianzi2000";

這樣的說明也確定了一個位址,對應於my_name. 假設位址是007E85DC,則記憶體中[007E85DC]='l',[007E85DD]='i', etc. 對my_name的訪問也就是對這位址處的資料訪問。

指標變數其本身也同樣對應一個位址,因為它本身也是一個變數。如:

char *your_name;

這時也確定變數"your_name"對應一個記憶體位址,假設為007E85F0. 語句your_name=my_name;很可能表現為:

mov [007E85F0], 007E85DC ;your_name的內容是my_name的位址。

2. 寄存器變數

在C和C++中允許說明寄存器變數。register int i; 指明i是寄存器存放的整型變數。通常,編譯器都把寄存器變數放在esi和edi中。寄存器是在cpu內部的結構,對它的訪問要比記憶體快得多,所以把頻繁使用的變數放在寄存器中可以提高程式執行速度。

3. 陣列

不管是多少維的陣列,在記憶體中總是把所有的元素都連續存放,所以在記憶體中總是一維的。例如,int i_array[2][3]; 在記憶體確定了一個位址,從該位址開始的12個位元組用來存貯該陣列的元素。所以變數名i_array對應著該陣列的起始位址,也即是指向陣列的第一個元素。存放的順序一般是i_array[0][0],[0][1],[0][2],[1][0],[1][1],[1][2] 即最右邊的下標變化最快。當需要訪問某個元素時,程式就會從多維索引值換算成一維索引,如訪問i_array[1][1],換算成記憶體中的一維索引值就是1*3+1=4.這種換算可能在編譯的時候就可以確定,也可能要到執行時才可以確定。無論如何,如果我們把i_array對應的位址裝入一個通用寄存器作為基址,則對陣列元素的訪問就是一個計算有效位址的問題:

; i_array[1][1]=0x16

lea ebx,xxxxxxxx ;i_array 對應的位址裝入ebx
mov edx,04 ;訪問i_array[1][1],編譯時就已經確定
mov word ptr [ebx+edx*2], 16 ;

當然,取決於不同的編譯器和程式上下文,具體實現可能不同,但這種基本的形式是確定的。從這裏也可以看到比例因數的作用(還記得比例因數的取值為1,2,4或8嗎?),因為在目前的系統中簡單變數總是佔據1,2,4或者8個位元組的長度,所以比例因數的存在為在記憶體中的查表操作提供了極大方便。

4. 結構和物件

結構和物件的成員在記憶體中也都連續存放,但有時為了在字邊界或雙字邊界對齊,可能有些微調整,所以要確定物件的大小應該用sizeof操作符而不應該把成員的大小相加來計算。當我們聲明一個結構變數或初始化一個物件時,這個結構變數和物件的名字也對應一個記憶體位址。舉例說明:
struct tag_info_struct
{
int age;
int sex;
float height;
float weight;
} marry;

變數marry就對應一個記憶體位址。在這個位址開始,有足夠多的位元組(sizeof(marry))容納所有的成員。每一個成員則對應一個相對於這個位址的偏移量。這裏假設此結構中所有的成員都連續存放,則age的相對位址為0,sex為2, height 為4,weight為8。

; marry.sex=0;

lea ebx,xxxxxxxx ;marry 對應的記憶體位址
mov word ptr [ebx+2], 0
......

物件的情況基本相同。注意成員函數具體的實現在代碼段中,在物件中存放的是一個指向該函數的指標。


5. 函數調用

一個函數在被定義時,也確定一個記憶體位址對應於函數名字。如:

long comb(int m, int n)
{
long temp;
.....

return temp;
}

這樣,函數comb就對應一個記憶體位址。對它的調用表現為:

CALL xxxxxxxx ;comb對應的位址。這個函數需要兩個整型參數,就通過堆疊來傳遞:

;lresult=comb(2,3);

push 3
push 2
call xxxxxxxx
mov dword ptr [yyyyyyyy], eax ;yyyyyyyy是長整型變數lresult的位址

這裏請注意兩點。第一,在C語言中,參數的壓棧順序是和參數順序相反的,即後面的參數先壓棧,所以先執行push 3. 第二,在我們討論的32位元系統中,如果不指明參數類型,缺省的情況就是壓入32位元雙字。因此,兩個push指令總共壓入了兩個雙字,即8個位元組的資料。然後執行call指令。call 指令又把返回位址,即下一條指令(mov dword ptr....)的32位位址壓入,然後跳轉到xxxxxxxx去執行。

在comb副程式入口處(xxxxxxxx),堆疊的狀態是這樣的:

03000000 (請回憶small endian 格式)
02000000
yyyyyyyy <--ESP 指向返回地址 前面講過,副程式的標準起始代碼是這樣的: push ebp ;保存原先的ebp mov ebp, esp;建立框架指針 sub esp, XXX;給臨時變數預留空間 ..... 執行push ebp之後,堆疊如下: 03000000 02000000 yyyyyyyy old ebp <---- esp 指向原來的ebp 執行mov ebp,esp之後,ebp 和esp 都指向原來的ebp. 然後sub esp, xxx 給臨時變數留空間。這裏,只有一個臨時變數temp,是一個長整數,需要4個位元組,所以xxx=4。這樣就建立了這個子程式的框架: 03000000 02000000 yyyyyyyy old ebp <---- 當前ebp指向這裏 temp 所以副程式可以用[ebp+8]取得第一參數(m),用[ebp+C]來取得第二參數(n),以此類推。臨時變數則都在ebp下面,如這裏的temp就對應於[ebp-4]. 副程式執行到最後,要返回temp的值: mov eax,[ebp-04] 然後執行相反的操作以撤銷框架: mov esp,ebp ;這時esp 和ebp都指向old ebp,臨時變數已經被撤銷 pop ebp ;撤銷框架指針,恢復原ebp. 這是esp指向返回位址。緊接的retn指令返回主程序: retn 4 該指令從堆疊彈出返回位址裝入EIP,從而返回到主程序去執行call後面的指令。同時調整 esp(esp=esp+4*2),從而撤銷參數,使堆疊恢復到調用副程式以前的狀態,這就是堆疊的平衡。調用副程式前後總是應該維持堆疊的平衡。從這裏也可以看到,臨時變數temp已經隨著副程式的返回而消失,所以試圖返回一個指向臨時變數的指標是非法的。 為了更好地支援高階語言,INTEL還提供了指令Enter 和Leave 來自動完成框架的建立和撤銷。Enter 接受兩個運算元,第一個指明給臨時變數預留的位元組數,第二個是副程式嵌套調用層數,一般都為0。enter xxx,0 相當於: push ebp mov ebp,esp sub esp,xxx leave 則相當於: mov esp,ebp pop ebp ============================================================= 好啦,我的學習心得講完了,謝謝各位的抬舉。教程是不敢當的,因為我也是個大菜鳥。如果這些東東能使你們的學習輕鬆一些,進步快一些,本菜鳥就很開心了。

--> 閱讀更多...

2009年3月17日 星期二

●我的新NB:ASUS N80VC 新居落成

到貨馬上二話不說,先安裝bbxp再說,安裝過程還稱的上順利。拜網路上有太多solution 所以2~3HR就完成ㄌ,當然我最重要的virtual pc、source insight、masm、nasm、ubuntu linux、也都要一一給它setup完成,所以我想未來會從網誌上開始加入一些個人所學的資料,一方面幫助記憶,另一方面檢視自己哪方面能力不足須加強。順便把一些舊資訊整理整理;最近用了Notepad++,覺得實在不錯,感覺效能上比pspad好很多,在此也建議有在用notepad記事本ㄉ大大們download來用用,免費open source喔;另外再介紹一ㄍ好用ㄉtools:檔案比對工具Araxis Merge ,雖然目前很多人多會使用ultra compare或beyond compare但我個人已經習慣這個ㄌ;這些都不是copyleft,所以大大們請各憑本事囉。上圖是 "新居落成"的desktop。
P.S.在linux也有個kdiff3的比對軟體。
--> 閱讀更多...