自己學習WDM果然還是有許多問題無法解決,也只能邊學邊做筆記,做的筆記也不知道是對是錯,只能說WDM真難搞,難怪外面的WDM課程都那麼貴。
//----------------------
// ACP CreateFile
//----------------------
CreateFile() 可以去取得某個Driver的Handle,而這個API使用的是WDM Driver中的外部名稱,但是一般Driver命名方式除了直接給名稱之外還可以使用GUID(一串數字),而這兩種差別在下面比較:
外部名稱:
(1) 直接命名 : \\DeviceDos\\MyDriver
(2) GUID : DEFINE_GUID GUID_DEVICE_MYDRIVER 1234-ABCD-7890123ABCD-....
所以如果Driver是使用GUID,則必須透過一些方式轉換成CreateFile()可以使用的格式。
//------------------------
// 外部名稱的取得
//------------------------
想要對某個Driver送IRP,首先就要利用CreateFile()取得Driver Handle,而這個函數又需要知道Driver的外部名稱,那如何知道Driver的外部名稱是什麼?
這邊又要把Driver分成兩種:
(1) Inbox Driver: 這種型態的驅動程式指的是OS本身提供的,那這些Driver幾乎都是GUID命名,而這個GUID可以在DDK中找到,找xxx.h內的宣告,或是直接搜尋關鍵字 "DEFINE_GUID" or "GUID_DEVICE_HDD"
其中"GUID_DEVICE_HDD"也可以是"GUID_DEVICE_BATTERY",看你是哪個設備的Driver你就打什麼名稱,如果找不到就要另外想辦法囉。
(2) Non-Inbox Driver : 這類的驅動程式就是指廠商自己寫的,例如ATI...etc. 而這類的驅動程式一般廠商會提供GUID,或是在廠商提供的xxx.inf 內搜尋。
//-------------------------------
// Release Driver
//-------------------------------
一般Release Driver會給3個檔案:
(1) xxx.inf : 這是類似Script的東西,用來把Driver的一些資訊告訴OS,也就是會安裝一些資訊到Registry之中(例如 : ...\Service\{ServiceName}),而INF檔案裡面有其格式,可參考MSDN說明,其中就包含Driver的GUID,另外在[DeviceList]下面宣告的就是所謂的Hardware ID。
(2) xxx.sys : Driver COnpiler好之後的檔案,也就是實際的程式碼檔案。
(3) xxx.cat : 當經過WHQL後會有這個檔案可以使用,OS就會認定這個Driver是屬於經過認證的驅動程式,當然也可以做一個假的來測試。
//--------------------------------
// Driver Stack
//--------------------------------
Filter-DO: 過濾Driver
Function-DO : 一般設備的驅動程式都稱為Function-DO
PDO: Bus Driver
一般PDO還可以看做下一層的Driver,因為OS會把Driver堆疊起來,所謂的堆疊就是利用DeviceObject內的某個LowerDeviceObject欄位指向另一個DeviceObject,而LowerDeviceObject 指過去的Driver就是PDO,也就是下一層Driver,如果剛好是Bus Driver,那麼PDO=Bus Driver...
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)
{
NTSTATUS ntStatus = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION),
NULL, FILE_DEVICE_UNKNOWN, 0, FALSE, &fdo);
....
pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);
....
}
由範例程式可以看到當PnP Manager 通知Driver去執行AddDevice()函數時,會跟我們的Driver說下一層的PDO是誰(當成參數傳給AddDevice()),然後我們先去為我們的Driver建立一個DeviceObject,這個DO也就是FDO,然後把我們的FDO 連接(Attach)在某個PDO上面,這樣子是不是就長的很像Stack ?
將自己踏入BIOS領域中所學習到的知識做一些心得整理,像是Legacy BIOS、EFI BIOS、Windows Driver...etc. ※版權與智慧財產權聲明:保留所有法律權利。我在寫文章時如果有引用到其他人的地方我會盡量說明參考出處,如果有遺漏的地方請告訴我,我會馬上註明! 而轉貼我的文章時也請您註明出處!
星期四, 8月 30, 2007
星期三, 8月 29, 2007
WDM 筆記
最近為了撰寫ACPI Driver所以先把之前WDM Driver的筆記整理一下:
//--------------------------
// IRP 用途
//--------------------------
IRP, I/O Request Packet : 內含command, data的一組封包。系統和Driver溝通時使用。
例如AP端送出IRP,內涵IOCTL_XXXX指令,以及一些參數
對應的Driver會去分析指令然後做對應的動作:
Switch(IRP->Command)
{
case IOCTL_XXXX :
Do Somethings here
case: ...
}
//---------------------------
//WDM Driver的Entry Point
//---------------------------
NTSTATUS
DriverEntry( IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
...
//在這邊去註冊你的Routine到DriverObject資料結構內
DriverObject->DriverUnload = MyIo_Unload;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MyIo_CreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyIo_Dispatcher;
...
}
//---------------------------
// WDM Driver 命名方式有兩種:
//---------------------------
1.內部名稱 (在你使用IoCreateDevice會用到) : Kernel 使用
\\Devices Kernel Mode定義
2.外部名稱(symbolic link name,給User應用程式的CreateFile使用) : User端使用
\\DosDevices
#define MYIO_NT_DEVICE_NAME L"\\Device\\MyIO"
#define MYIO_DOS_DEVICE_NAME L"\\DosDevices\\MyIO"
簡單的說就是OS 會把我們寫的Driver載入到記憶體中,然後會幫我們產生一個Driver Object資料結構在記憶體中,然後把這個資料結構位址當作參數傳給我們的DriverEntry(),接著我們就會設定資料結構中的一些函數指標指向我們Driver記憶體中的函數位址。EX: DriverObject->DriverUnload = MyIo_Unload;
當Driver Object結構設定好之後(或稱註冊)我們會去呼叫IoCreateDevice()函數,這個函數會在OS管理的Namespace中插入一個名稱,然後Kernel Driver層如果要找我們Driver時候就會去找這個名稱,而這個名稱就是內部名稱。
另外當應用程式需要對我們撰寫driver送出一些IRP的時候,其實也是透過"名稱"的方式來找到我們註冊進去的函數,然後OS才會把我們的IRP送給Driver Object內指標所指向的函數來處理,因此為了不要混亂掉,所以用另一種命名方式給應用程式使用,這就是外部名稱。
//-----------------------
// WDM Driver被載入的時機
//-----------------------
1. OpenSCM 方式
我們自己寫的Driver可以不用到到Windows目錄下,當我們需要這個Driver來使用的時候,可以透過SCM所提供的一些服務,先在登入檔SYSTEM\CurrentControlSet\Services 下面替我們的Driver建立一個鍵值,然後利用SCM 的服務去把我們的Driver載入到記憶體中,接著呼叫DriverEntry(),之後我們就可以透過AP對這個Driver 送出IRP
2. PnP Driver方式
對照 Registry 和 INF 檔,找到對應的driver,因為每個PnP Device都有自己的ID,類似PCI ID或是Vendor ID...等,當系統偵測到Device時,就會去登入檔內找到對應的Driver,所以自己撰寫的Driver也要撰寫一個xxx.inf 用來安裝這些資訊。
(1) 當系統偵測到device,就會將對應的driver載入到記憶體中,接著呼叫driver的DeviceEntry()
(2) 接著系統的PnP Manager 會去呼叫driver中的AddDevice() (註冊在Driver Object中)
(3) 處理AP的IRP或是PnP Manager 的IRP,或是Device 的INT 請求...等
3.非 PnP Driver
也是對照Registry 和INF 檔找到對應的Driver,但是與PnP Driver差別在於沒有PnP ID,因此OS不會自動找到Driver且載入Driver,而是User自己要利用新增設備(Add New Hardware Wizard)來增加Device,接著OS也是對照Registry 和INF 檔找到對應的Driver來載入到記憶體中。
//--------------------------
// IRP 用途
//--------------------------
IRP, I/O Request Packet : 內含command, data的一組封包。系統和Driver溝通時使用。
例如AP端送出IRP,內涵IOCTL_XXXX指令,以及一些參數
對應的Driver會去分析指令然後做對應的動作:
Switch(IRP->Command)
{
case IOCTL_XXXX :
Do Somethings here
case: ...
}
//---------------------------
//WDM Driver的Entry Point
//---------------------------
NTSTATUS
DriverEntry( IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
...
//在這邊去註冊你的Routine到DriverObject資料結構內
DriverObject->DriverUnload = MyIo_Unload;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MyIo_CreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyIo_Dispatcher;
...
}
//---------------------------
// WDM Driver 命名方式有兩種:
//---------------------------
1.內部名稱 (在你使用IoCreateDevice會用到) : Kernel 使用
\\Devices Kernel Mode定義
2.外部名稱(symbolic link name,給User應用程式的CreateFile使用) : User端使用
\\DosDevices
#define MYIO_NT_DEVICE_NAME L"\\Device\\MyIO"
#define MYIO_DOS_DEVICE_NAME L"\\DosDevices\\MyIO"
簡單的說就是OS 會把我們寫的Driver載入到記憶體中,然後會幫我們產生一個Driver Object資料結構在記憶體中,然後把這個資料結構位址當作參數傳給我們的DriverEntry(),接著我們就會設定資料結構中的一些函數指標指向我們Driver記憶體中的函數位址。EX: DriverObject->DriverUnload = MyIo_Unload;
當Driver Object結構設定好之後(或稱註冊)我們會去呼叫IoCreateDevice()函數,這個函數會在OS管理的Namespace中插入一個名稱,然後Kernel Driver層如果要找我們Driver時候就會去找這個名稱,而這個名稱就是內部名稱。
另外當應用程式需要對我們撰寫driver送出一些IRP的時候,其實也是透過"名稱"的方式來找到我們註冊進去的函數,然後OS才會把我們的IRP送給Driver Object內指標所指向的函數來處理,因此為了不要混亂掉,所以用另一種命名方式給應用程式使用,這就是外部名稱。
//-----------------------
// WDM Driver被載入的時機
//-----------------------
1. OpenSCM 方式
我們自己寫的Driver可以不用到到Windows目錄下,當我們需要這個Driver來使用的時候,可以透過SCM所提供的一些服務,先在登入檔SYSTEM\CurrentControlSet\Services 下面替我們的Driver建立一個鍵值,然後利用SCM 的服務去把我們的Driver載入到記憶體中,接著呼叫DriverEntry(),之後我們就可以透過AP對這個Driver 送出IRP
2. PnP Driver方式
對照 Registry 和 INF 檔,找到對應的driver,因為每個PnP Device都有自己的ID,類似PCI ID或是Vendor ID...等,當系統偵測到Device時,就會去登入檔內找到對應的Driver,所以自己撰寫的Driver也要撰寫一個xxx.inf 用來安裝這些資訊。
(1) 當系統偵測到device,就會將對應的driver載入到記憶體中,接著呼叫driver的DeviceEntry()
(2) 接著系統的PnP Manager 會去呼叫driver中的AddDevice() (註冊在Driver Object中)
(3) 處理AP的IRP或是PnP Manager 的IRP,或是Device 的INT 請求...等
3.非 PnP Driver
也是對照Registry 和INF 檔找到對應的Driver,但是與PnP Driver差別在於沒有PnP ID,因此OS不會自動找到Driver且載入Driver,而是User自己要利用新增設備(Add New Hardware Wizard)來增加Device,接著OS也是對照Registry 和INF 檔找到對應的Driver來載入到記憶體中。
標籤:
Windows 程式相關
星期一, 7月 30, 2007
動態記憶體(3)-撰寫輔助程式
前面兩篇文章提到如何去找到動態記憶體指標的程式碼位址,然後注入一段程式碼來取得指標ESI的位址,如果你在Softice 中修改後,並且回到遊戲中去驗證沒問題後(驗證的方式就是回到遊戲中隨便更新一下血量,或是重新啟動遊戲讓他重新配置一個指標位址,接著你可以利用GameMaster直接去讀取57BF30h這個位址的內容,這個內容=ESI,所以你要自己加上228h 的位址才會存放你的血量值),你就可以開始動手寫輔助程式了。
ESI+228h=血量位址
如何撰寫輔助程式呢? 這邊會用到的工具:
VC6 or VB6 or...等 (看你自己習慣用哪一種)
Windosw API (自己翻手冊查相關說明)
Softice (用來看機器碼)
簡單說就是利用ReadProcessMemory 或是WriteProcessMemory方式去對記憶體寫入值,而寫入的值是一堆的機器碼(因為你沒辦法像Softice一樣直接修改,所以只好用機器碼的方式注入你的程式碼到修改的位址去),所以你透過SoftIce 去修改好程式碼後,要記得把機器碼抄錄下來(例如: D 44717A),然後用這兩個API寫進去你要修改的位址。
VC6的方式 :
#define MY_CODE1 0xE9
#define MY_CODE2 0x34 //這部分是要寫入的機器碼的常量定義
.........................
//-----------------------------------------------------------------------------//
DWORD A1 =MY_CODE1;
DWORD A2 =MY_CODE2;
..............
//0x0044717A
WriteProcessMemory(nOK,(LPVOID)0x0044717A+0,&A1,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+1,&A2,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+2,&A3,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+3,&A4,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+4,&A5,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+5,&A6,1,NULL);
//0057BF00
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+0,&B1,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+1,&B2,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+2,&B3,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+3,&B4,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+4,&B5,1,NULL);
....
VB6方式:
A_Start = &H0044717A '要修改成JMP的地址
A_End = &H0057BF00 '跳躍JMP到的地址
'&H0044717A
Call WriteProcessMemory(A_Start + 0, &HE9, 1)
Call WriteProcessMemory(A_Start + 1, &H3D, 1)
Call WriteProcessMemory(A_Start + 2, &H31, 1)
Call WriteProcessMemory(A_Start + 3, &H30, 1)
...
ESI+228h=血量位址
如何撰寫輔助程式呢? 這邊會用到的工具:
VC6 or VB6 or...等 (看你自己習慣用哪一種)
Windosw API (自己翻手冊查相關說明)
Softice (用來看機器碼)
簡單說就是利用ReadProcessMemory 或是WriteProcessMemory方式去對記憶體寫入值,而寫入的值是一堆的機器碼(因為你沒辦法像Softice一樣直接修改,所以只好用機器碼的方式注入你的程式碼到修改的位址去),所以你透過SoftIce 去修改好程式碼後,要記得把機器碼抄錄下來(例如: D 44717A),然後用這兩個API寫進去你要修改的位址。
VC6的方式 :
#define MY_CODE1 0xE9
#define MY_CODE2 0x34 //這部分是要寫入的機器碼的常量定義
.........................
//-----------------------------------------------------------------------------//
DWORD A1 =MY_CODE1;
DWORD A2 =MY_CODE2;
..............
//0x0044717A
WriteProcessMemory(nOK,(LPVOID)0x0044717A+0,&A1,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+1,&A2,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+2,&A3,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+3,&A4,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+4,&A5,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0044717A+5,&A6,1,NULL);
//0057BF00
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+0,&B1,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+1,&B2,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+2,&B3,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+3,&B4,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0057BF00+4,&B5,1,NULL);
....
VB6方式:
A_Start = &H0044717A '要修改成JMP的地址
A_End = &H0057BF00 '跳躍JMP到的地址
'&H0044717A
Call WriteProcessMemory(A_Start + 0, &HE9, 1)
Call WriteProcessMemory(A_Start + 1, &H3D, 1)
Call WriteProcessMemory(A_Start + 2, &H31, 1)
Call WriteProcessMemory(A_Start + 3, &H30, 1)
...
標籤:
Windows 程式相關
動態記憶體(2)-修改流程
前一篇文章中我們介紹了如何透過一些工具去取得你的遊戲程式碼的位址,接著就說明一下整個修改的概念:
1. 先找到程式碼存放的位址,前面我們已經找到了程式碼存放的位址是在44717Ah:
001B:44717A MOV [ESI+228],EDX
001b:44717E ....
2. 修改44717A這個位址的程式碼
001B:44717A JMP 57BF00 <--跳到我們指定的一塊記憶體位址
001B:44717D NOP <--由於修改的JMP 57BF00 指令長度不足原來的,所以增加NOP來補足長度
001b:44717E ....
3. 修改57BF00
001B:57BF00 MOV [ESI+228],EDX <--還原原來被我們破壞掉的程式碼
001B:57BFxx MOV DWORD PTR [57BF30],ESI
001B:57BFxx JMP 44717E
001B:57BFxx
由上面的範例中可以看到整個修改的流程大致上就是如此,先找到他的程式碼的位址,修改它的程式碼,然後跳躍到我們指定的記憶體中,跳躍後我們先去還原原來的程式碼以免造成原來遊戲程式當機,接著把ESI的值存放到我們指定的記憶體位址,那麼當遊戲程式重新分配記憶體位址時,他的位址會放在ESI,而ESI的值會被我們放到我們指定的記憶體中,而我們寫的遊戲輔助程式就直接去我們指定的記憶體中就可以讀到ESI的值了。
※ Softice 修改程式碼的方式鍵入 "A 44717A" ,跟DOS的Debug.com 很像
在上述的方式中,要如何找到一塊可以使用的記憶體? 下面我就提供一個方式:
進入Softice 中,找到你的遊戲視窗名稱,接著鍵入MAP32 指令,如下所示:
:map32 lineage <--lineage 是你遊戲的名稱,這邊是範例,你看的值會不一樣
Owner Obj Name Obj# Address Size Type
lineage .text 0001 017F:00401000 ....
lineage .data 0002 0187:00560000 ....
lineage .rsrc 0005 0187:0057C000 ....
你會看到他把遊戲程式的PE檔頭的一些資訊列印出來給你看,其中.data 後面的560000h代表你的遊戲會把一些資料從記憶體位置560000h 開始存放,而我的方式是從後面自己找一塊來用,例如下一個區塊是從57C000h開始,因此我找57BF00h~57BFFFh 這一塊來當我要使用的地方。
你可以利用Softice 鍵入D 57BF00的方式去看那一塊記憶體有沒有人使用,如果沒有的話你就可以自己拿來用。
1. 先找到程式碼存放的位址,前面我們已經找到了程式碼存放的位址是在44717Ah:
001B:44717A MOV [ESI+228],EDX
001b:44717E ....
2. 修改44717A這個位址的程式碼
001B:44717A JMP 57BF00 <--跳到我們指定的一塊記憶體位址
001B:44717D NOP <--由於修改的JMP 57BF00 指令長度不足原來的,所以增加NOP來補足長度
001b:44717E ....
3. 修改57BF00
001B:57BF00 MOV [ESI+228],EDX <--還原原來被我們破壞掉的程式碼
001B:57BFxx MOV DWORD PTR [57BF30],ESI
001B:57BFxx JMP 44717E
001B:57BFxx
由上面的範例中可以看到整個修改的流程大致上就是如此,先找到他的程式碼的位址,修改它的程式碼,然後跳躍到我們指定的記憶體中,跳躍後我們先去還原原來的程式碼以免造成原來遊戲程式當機,接著把ESI的值存放到我們指定的記憶體位址,那麼當遊戲程式重新分配記憶體位址時,他的位址會放在ESI,而ESI的值會被我們放到我們指定的記憶體中,而我們寫的遊戲輔助程式就直接去我們指定的記憶體中就可以讀到ESI的值了。
※ Softice 修改程式碼的方式鍵入 "A 44717A" ,跟DOS的Debug.com 很像
在上述的方式中,要如何找到一塊可以使用的記憶體? 下面我就提供一個方式:
進入Softice 中,找到你的遊戲視窗名稱,接著鍵入MAP32 指令,如下所示:
:map32 lineage <--lineage 是你遊戲的名稱,這邊是範例,你看的值會不一樣
Owner Obj Name Obj# Address Size Type
lineage .text 0001 017F:00401000 ....
lineage .data 0002 0187:00560000 ....
lineage .rsrc 0005 0187:0057C000 ....
你會看到他把遊戲程式的PE檔頭的一些資訊列印出來給你看,其中.data 後面的560000h代表你的遊戲會把一些資料從記憶體位置560000h 開始存放,而我的方式是從後面自己找一塊來用,例如下一個區塊是從57C000h開始,因此我找57BF00h~57BFFFh 這一塊來當我要使用的地方。
你可以利用Softice 鍵入D 57BF00的方式去看那一塊記憶體有沒有人使用,如果沒有的話你就可以自己拿來用。
標籤:
Windows 程式相關
動態記憶體(1)-搜尋程式碼
玩過PC遊戲的都知道市面上有一些遊戲修改器可以做一些修改遊戲記憶體內的資訊來達到你的主角能力值變強或是有用不完的金錢...等。
今天我們要探討的是網路遊戲的修改,其實不能算是修改啦,因為對於網路遊戲來說,你無法使用這種方式去修改相關資訊,因為你的主角資訊是放在伺服器端,然後透過網路傳送資訊到你的主機,接著再更新到你的主機上面的記憶體中,例如: 血量:
CMP EDX,EDX
JZ LABLE1
MOV [ESI+228],EDX
...
LABEL1:
...
例如上面的範例,他會去檢查血量有沒有變化,如果有他才會放進去[ESI+228]的記憶體位址中,因此EDX存放著伺服器端傳送過來的血量資料,然後放到你的主機的記憶體位址[ESI+228]的地方,這邊的記憶體位址指的是位址空間,所謂的位址空間是指每個行程都有4G的虛擬記憶體位址,就好比說VC寫的程式在載入記憶體的時候會被固定放在某個位址上去,然後我們就可以知道某段位址範圍放的是"程式碼"。
上面有看到一個[ESI+228]的記憶體位址,由於網路遊戲怕你很容易就找到他的記憶體位址,所以一般會把這個位址用指標方式來重新配置記憶體,簡單說就是你每次找到血量存放的位址會不一樣,所以稱為動態記憶體方式來存放人物資訊,也因此我們要討論的是如何得到這個指標指向的位址,那麼就不用怕他一直變動了,所以由上面的範例可以知道ESI會一直變動,所以我們的目標就是得到ESI的值。
※ESI+228h = 血量位址,每個遊戲不一樣,所以不見得都是用ESI來存放,另外也不見得是+228h
在進入正題前先說明如何去得知你的遊戲是如何把這些資訊放進去記憶體中,我們會使用的工具如下:
1.WinXP
2.SoftIce
3.任何遊戲修改工具 ex: FPE, GameMaster...等
(Step 1): 首先先開啟你的網路遊戲,然後利用遊戲修改工具找到你的血量的記憶體位址(找的方式跟以前一樣,就是先找一次血量,然後進入遊戲改變一下血量,然後再找一次,一直重複到剩下來的位址,或是利用人物ID去搜尋也可以),例如找到的位址是12ABAC00h, 然後按下Ctrl+D 進入SoftIce,進入SoftIce後鍵入BPM 12ABAC00 W ,意思是說設定一個中斷點,當這個記憶體位址12ABAC00 被寫入Write的時候要中斷,接著按下Ctrl+D 回到遊戲中
(Step 2): 回到遊戲後隨便改變一下你的血量(喝藥水或是放法術),然後你就會看到SoftIce 產生了一個中斷,然後進入到SoftIce 畫面(如果有攔截到,就會自動進入SoftIce,如果沒攔截到就可以能你找到的位址不對)
(Step 3): 在SoftIce 中你會看到類似下面的程式碼:
001B:44717A MOV [ESI+228],EDX
001b:44717E ....
他所代表的意思是說你的遊戲主程式載入到記憶體後負責更新血量到記憶體位址12ABAC00h的程式碼是被放在xxxx:44717A ,而程式碼是 MOV [ESI+228],EDX,
你可以在Softice 中鍵入指令 D ESI+228 ,他會把ESI+228的記憶體位址傾印出來,所以你可以在螢幕上看到你的血量值。
(Step 4) : 拿筆把44717A 這個位址抄下來,然後把你的遊戲關閉掉然後重新啟動,重新進入遊戲後重複放面的Step1~3 ,如果每次都是停在44717A,那麼就恭喜你,你已經找到他的動態指標的程式碼的地方,接著就可以想辦法取得ESI 的值了。
今天我們要探討的是網路遊戲的修改,其實不能算是修改啦,因為對於網路遊戲來說,你無法使用這種方式去修改相關資訊,因為你的主角資訊是放在伺服器端,然後透過網路傳送資訊到你的主機,接著再更新到你的主機上面的記憶體中,例如: 血量:
CMP EDX,EDX
JZ LABLE1
MOV [ESI+228],EDX
...
LABEL1:
...
例如上面的範例,他會去檢查血量有沒有變化,如果有他才會放進去[ESI+228]的記憶體位址中,因此EDX存放著伺服器端傳送過來的血量資料,然後放到你的主機的記憶體位址[ESI+228]的地方,這邊的記憶體位址指的是位址空間,所謂的位址空間是指每個行程都有4G的虛擬記憶體位址,就好比說VC寫的程式在載入記憶體的時候會被固定放在某個位址上去,然後我們就可以知道某段位址範圍放的是"程式碼"。
上面有看到一個[ESI+228]的記憶體位址,由於網路遊戲怕你很容易就找到他的記憶體位址,所以一般會把這個位址用指標方式來重新配置記憶體,簡單說就是你每次找到血量存放的位址會不一樣,所以稱為動態記憶體方式來存放人物資訊,也因此我們要討論的是如何得到這個指標指向的位址,那麼就不用怕他一直變動了,所以由上面的範例可以知道ESI會一直變動,所以我們的目標就是得到ESI的值。
※ESI+228h = 血量位址,每個遊戲不一樣,所以不見得都是用ESI來存放,另外也不見得是+228h
在進入正題前先說明如何去得知你的遊戲是如何把這些資訊放進去記憶體中,我們會使用的工具如下:
1.WinXP
2.SoftIce
3.任何遊戲修改工具 ex: FPE, GameMaster...等
(Step 1): 首先先開啟你的網路遊戲,然後利用遊戲修改工具找到你的血量的記憶體位址(找的方式跟以前一樣,就是先找一次血量,然後進入遊戲改變一下血量,然後再找一次,一直重複到剩下來的位址,或是利用人物ID去搜尋也可以),例如找到的位址是12ABAC00h, 然後按下Ctrl+D 進入SoftIce,進入SoftIce後鍵入BPM 12ABAC00 W ,意思是說設定一個中斷點,當這個記憶體位址12ABAC00 被寫入Write的時候要中斷,接著按下Ctrl+D 回到遊戲中
(Step 2): 回到遊戲後隨便改變一下你的血量(喝藥水或是放法術),然後你就會看到SoftIce 產生了一個中斷,然後進入到SoftIce 畫面(如果有攔截到,就會自動進入SoftIce,如果沒攔截到就可以能你找到的位址不對)
(Step 3): 在SoftIce 中你會看到類似下面的程式碼:
001B:44717A MOV [ESI+228],EDX
001b:44717E ....
他所代表的意思是說你的遊戲主程式載入到記憶體後負責更新血量到記憶體位址12ABAC00h的程式碼是被放在xxxx:44717A ,而程式碼是 MOV [ESI+228],EDX,
你可以在Softice 中鍵入指令 D ESI+228 ,他會把ESI+228的記憶體位址傾印出來,所以你可以在螢幕上看到你的血量值。
(Step 4) : 拿筆把44717A 這個位址抄下來,然後把你的遊戲關閉掉然後重新啟動,重新進入遊戲後重複放面的Step1~3 ,如果每次都是停在44717A,那麼就恭喜你,你已經找到他的動態指標的程式碼的地方,接著就可以想辦法取得ESI 的值了。
標籤:
Windows 程式相關
星期四, 7月 12, 2007
Source Insight v.s SlickEdit® 2007
============================
Program Editor and Analyzer
============================
最近為了撰寫程式碼上的方便,分別去試用了兩套軟體,這兩套軟體都是用來撰寫程式碼與分析程式碼的編輯器,下面就大致上來說一下使用心得。
Source Insight : 目前版本最新版是V3.50.0057 ,他跟一般的程式碼編輯器比較起來功能強很多,像是你可以在變數名稱的關鍵字上面按下滑鼠右鍵就可以很方便的移動到定義(Go to definition)的地方或是列舉出參考(Reference)到的地方,另外針對函數或是變數...等,都有很不錯的顏色來區分,另外它還可以執行一些外部程式,我自己就用VB6寫了一些外部程式,然後按下熱鍵就可以呼叫我的程式來輔助編輯,像是說常常我們需要寫註解,那麼你就可以用Vb6 寫一段程式碼呼叫Postmessage API 對Source Insight 編輯區插入一段字串而省去打字的時間,另外專案檔的管理與搜尋也別具特色,他有檔案瀏覽器,你可以直接輸入你要找的檔案名稱,然後會列表出來,接著你就可以用滑鼠去點擊檔案來編輯,缺點就是字型太小,如果改大一點會變亂掉,另外就是不能編輯Unicode,且編輯畫面太小,附加的功能要自己利用其他語言工具開發...等。
SlickEdit® 2007 : 基本上Source Insight 有的他都有,編輯器長的很像UltraEdit,但是有比UltraEdit更強大的功能,像是Tag的建立與搜尋速度,可以連結VC6或是VisualStudio 直接Build或是Compiler你的程式碼,程式碼的分析與Symbols 搜尋也很快,缺點就是對於顏色的區分要自己手動建立,而且函數後面的括號多空一個就不會被當成函數顯示,不過這個缺點也是優點,因為他有提供Alias ,Macro...方式去增加你自己想要的功能,而且可以全螢幕編輯,畫面字體也大很多,簡言之就是他很多東西都要自己手動調整,如果你會調,調出來的功能一定比Source Insight 強大,所以使用後的心得是如果你貪圖方便,就用Source Insight就應該夠用了,如果你沒辦法忍受Source Insight編輯器畫面,又沒辦法忍受UltraEdit龜速的分析程式碼或是搜尋程式碼相關性的地方,那麼你就一定要考慮SlickEdit® 2007。
有人會想說幹麻自己去找編輯器,一般VC6或是Visual Studio不是就已經整合到IDE中了嗎 ? 沒錯,他們都已經有了,不過只能針對C/C++ 編輯,像我這種有.asm .c .cpp .inf .xxx 混合的專案就沒辦法編輯與分析了,因此要自己找一個編輯器可以用來管理分析程式碼,因此比較來比較去,就是這兩套最好用,而試用版在他們官方網站就可以找到了。
底下是一些設定的方式:
字體:
Tools->Options->Font
個人覺得最好看的是FixedSys。
顯示改動過的行標記:
Tools->Options->File Extension Setup
Advanced-Color coding-Modified lines
自動在保存的時候清除修改標記:
Tools->Options->File Options
Save-Reset line modify
這個標籤頁還有兩個實用的選項:
Expand tabs to spaces,保存時把Tab變成空格(有的地方要求必須用空格縮進)
Strip trailing spaces,刪掉行末尾的空白
快點顯示Symbol的代碼資訊:
下面的Symbol視窗類似與Source Insight的上下文視窗,會顯示當前函數或者巨集的代碼。
Tools->Options->Tagging Options
Updtae toolbars after ms idle:
縮短顯示代碼的速度。我用150。機子夠快扛得住就弄小點。
修改該死的自動語法範本:
Tools->Options->File Extension Options
Indent-Syntax expansion-Minmum expandable keyword length:
建議設置為3,如果為默認的1的話,你根本不能用i做變數。因為只要i起頭後面一個空格就是一個if () {},吐血。
滑鼠移到函數或者識別字上面顯示資訊:
Tools->Options->File Extension Options
Tagging-Show info for symbol under mouse
這部分其他的鉤也可以選上,減輕工作量啊。
對當前檔類型都顯示行號:
Tools->Options->File Extension Setup
General-Display line numbers
修改行號顏色:
Tools->Options->Color
不是Line Number,而是Line Prefix Area~~吐血 -_-!
改GCC的設置:
必須在建立了GNU C/C++專案後才能在Build裏面找到GNU C Options
設置專案的Build之類的功能表命令:
Project->Project Properties->Tools
選中一個Configuration以後就能配置編譯器和調試器
另外,在下面的Build視窗可以直接敲命令。比如make,dir什麼的,很方便
同時啟動多個SlickEdit
使用隱藏的命令開關“+new”即可。只要把SlickEdit的快捷方式修改成這樣:
"E:\Program Files\SlickEdit\win\vs.exe" +new
解決Slickedit某些視窗字體模糊的問題
比如Open Form的窗口,
用UE打開vs.exe、vsapi.dll兩個文件,查找“OEM Fixed Font”,會找到有OEM Fixed Font..Terminal的地方。把Terminal改成Fixedsys,注意要用覆蓋模式,不能改變了檔的長度,然後存檔即可。
將Slickedit的Projects資源列表切換成檔夾模式
Slickedit的檔排列是按照Source,Head之類的分的,很不習慣,把它改成按照檔夾結構
在Projects視窗中點擊一個vpj檔,按右鍵;選擇Auto Folder→Directory View,會出來一個警告框。再點OK就好了
Workspace分類管理
Workspace多了以後分類管理會比較方便
Project->Organize all workspace,可以用目錄的形式對workspace進行管理,
注意這裏並不會改變你workspace檔的位置,只是在slickedit裏面的分類而已。
如果發現路徑的'\'顯示成了中文的'¥'字元,那麼可以設置一下字體就可以了如果在XP下,直接將所有的字體都設置成 Default Fixed Font 就不錯。
增加Alias
選取一段文字-->按滑鼠右鍵選Create Alias
編輯滑鼠右鍵的Pop Menu
按滑鼠右鍵-->選Edit this Menu
如果你要把你的設定檔給別人使用,那麼你可以直接複製C:\Documents and Settings\你的使用者名稱\My Documents\My SlickEdit Config\內的 "12.0.2"這個目錄,但是要切記編輯滑鼠右鍵的Pop Menu
中的Version Control選項不能夠刪除掉,不然你複製過去的組態會不能使用,而且會出現錯誤訊息。
[註] 破解版請自行使用Google搜尋,或是使用IDA Pro ...等反組譯工具去修正某個DLL跟EXE就可以了...
不過我還是要說一句話,請尊重智慧財產權,愛用正版....感恩喔!
Reference
Source Insight V3.50.0057
http://sourceinsight.com/
SlickEdit® 2007 Version 12.0.2.0
http://www.slickedit.com/content/view/73/60/
Program Editor and Analyzer
============================
最近為了撰寫程式碼上的方便,分別去試用了兩套軟體,這兩套軟體都是用來撰寫程式碼與分析程式碼的編輯器,下面就大致上來說一下使用心得。
Source Insight : 目前版本最新版是V3.50.0057 ,他跟一般的程式碼編輯器比較起來功能強很多,像是你可以在變數名稱的關鍵字上面按下滑鼠右鍵就可以很方便的移動到定義(Go to definition)的地方或是列舉出參考(Reference)到的地方,另外針對函數或是變數...等,都有很不錯的顏色來區分,另外它還可以執行一些外部程式,我自己就用VB6寫了一些外部程式,然後按下熱鍵就可以呼叫我的程式來輔助編輯,像是說常常我們需要寫註解,那麼你就可以用Vb6 寫一段程式碼呼叫Postmessage API 對Source Insight 編輯區插入一段字串而省去打字的時間,另外專案檔的管理與搜尋也別具特色,他有檔案瀏覽器,你可以直接輸入你要找的檔案名稱,然後會列表出來,接著你就可以用滑鼠去點擊檔案來編輯,缺點就是字型太小,如果改大一點會變亂掉,另外就是不能編輯Unicode,且編輯畫面太小,附加的功能要自己利用其他語言工具開發...等。
SlickEdit® 2007 : 基本上Source Insight 有的他都有,編輯器長的很像UltraEdit,但是有比UltraEdit更強大的功能,像是Tag的建立與搜尋速度,可以連結VC6或是VisualStudio 直接Build或是Compiler你的程式碼,程式碼的分析與Symbols 搜尋也很快,缺點就是對於顏色的區分要自己手動建立,而且函數後面的括號多空一個就不會被當成函數顯示,不過這個缺點也是優點,因為他有提供Alias ,Macro...方式去增加你自己想要的功能,而且可以全螢幕編輯,畫面字體也大很多,簡言之就是他很多東西都要自己手動調整,如果你會調,調出來的功能一定比Source Insight 強大,所以使用後的心得是如果你貪圖方便,就用Source Insight就應該夠用了,如果你沒辦法忍受Source Insight編輯器畫面,又沒辦法忍受UltraEdit龜速的分析程式碼或是搜尋程式碼相關性的地方,那麼你就一定要考慮SlickEdit® 2007。
有人會想說幹麻自己去找編輯器,一般VC6或是Visual Studio不是就已經整合到IDE中了嗎 ? 沒錯,他們都已經有了,不過只能針對C/C++ 編輯,像我這種有.asm .c .cpp .inf .xxx 混合的專案就沒辦法編輯與分析了,因此要自己找一個編輯器可以用來管理分析程式碼,因此比較來比較去,就是這兩套最好用,而試用版在他們官方網站就可以找到了。
底下是一些設定的方式:
字體:
Tools->Options->Font
個人覺得最好看的是FixedSys。
顯示改動過的行標記:
Tools->Options->File Extension Setup
Advanced-Color coding-Modified lines
自動在保存的時候清除修改標記:
Tools->Options->File Options
Save-Reset line modify
這個標籤頁還有兩個實用的選項:
Expand tabs to spaces,保存時把Tab變成空格(有的地方要求必須用空格縮進)
Strip trailing spaces,刪掉行末尾的空白
快點顯示Symbol的代碼資訊:
下面的Symbol視窗類似與Source Insight的上下文視窗,會顯示當前函數或者巨集的代碼。
Tools->Options->Tagging Options
Updtae toolbars after ms idle:
縮短顯示代碼的速度。我用150。機子夠快扛得住就弄小點。
修改該死的自動語法範本:
Tools->Options->File Extension Options
Indent-Syntax expansion-Minmum expandable keyword length:
建議設置為3,如果為默認的1的話,你根本不能用i做變數。因為只要i起頭後面一個空格就是一個if () {},吐血。
滑鼠移到函數或者識別字上面顯示資訊:
Tools->Options->File Extension Options
Tagging-Show info for symbol under mouse
這部分其他的鉤也可以選上,減輕工作量啊。
對當前檔類型都顯示行號:
Tools->Options->File Extension Setup
General-Display line numbers
修改行號顏色:
Tools->Options->Color
不是Line Number,而是Line Prefix Area~~吐血 -_-!
改GCC的設置:
必須在建立了GNU C/C++專案後才能在Build裏面找到GNU C Options
設置專案的Build之類的功能表命令:
Project->Project Properties->Tools
選中一個Configuration以後就能配置編譯器和調試器
另外,在下面的Build視窗可以直接敲命令。比如make,dir什麼的,很方便
同時啟動多個SlickEdit
使用隱藏的命令開關“+new”即可。只要把SlickEdit的快捷方式修改成這樣:
"E:\Program Files\SlickEdit\win\vs.exe" +new
解決Slickedit某些視窗字體模糊的問題
比如Open Form的窗口,
用UE打開vs.exe、vsapi.dll兩個文件,查找“OEM Fixed Font”,會找到有OEM Fixed Font..Terminal的地方。把Terminal改成Fixedsys,注意要用覆蓋模式,不能改變了檔的長度,然後存檔即可。
將Slickedit的Projects資源列表切換成檔夾模式
Slickedit的檔排列是按照Source,Head之類的分的,很不習慣,把它改成按照檔夾結構
在Projects視窗中點擊一個vpj檔,按右鍵;選擇Auto Folder→Directory View,會出來一個警告框。再點OK就好了
Workspace分類管理
Workspace多了以後分類管理會比較方便
Project->Organize all workspace,可以用目錄的形式對workspace進行管理,
注意這裏並不會改變你workspace檔的位置,只是在slickedit裏面的分類而已。
如果發現路徑的'\'顯示成了中文的'¥'字元,那麼可以設置一下字體就可以了如果在XP下,直接將所有的字體都設置成 Default Fixed Font 就不錯。
增加Alias
選取一段文字-->按滑鼠右鍵選Create Alias
編輯滑鼠右鍵的Pop Menu
按滑鼠右鍵-->選Edit this Menu
如果你要把你的設定檔給別人使用,那麼你可以直接複製C:\Documents and Settings\你的使用者名稱\My Documents\My SlickEdit Config\內的 "12.0.2"這個目錄,但是要切記編輯滑鼠右鍵的Pop Menu
中的Version Control選項不能夠刪除掉,不然你複製過去的組態會不能使用,而且會出現錯誤訊息。
[註] 破解版請自行使用Google搜尋,或是使用IDA Pro ...等反組譯工具去修正某個DLL跟EXE就可以了...
不過我還是要說一句話,請尊重智慧財產權,愛用正版....感恩喔!
Reference
Source Insight V3.50.0057
http://sourceinsight.com/
SlickEdit® 2007 Version 12.0.2.0
http://www.slickedit.com/content/view/73/60/
星期五, 7月 06, 2007
System Shutdown
有些人對作業系統關機很有興趣,我這邊就大概描述一下BIOS在關機的動作中做了什麼事情,以及如何利用軟體工具去撰寫一段關機的程式碼。
一般在NoteBook上面控制電源的部分都是EC BIOS所控制,而有時候我們會透過Send EC Cmd方式給EC BIOS來達到關機的目的(Power Off)。
像是某些BIOS中會使用到關機的地方就是在Setup Menu-->Exit-->Shutdown 選項,而這個地方的BIOS code 就可以利用EC Cmd或是PM Register來達到關機的目的。
另外在進入作業系統後我們也可以透過上面的方式來強制關閉系統,所使用的方式就是利用PM Control Register。
所以BIOS扮演的角色就是去設定PM Register IO Address Range & ACPI Table ,這樣說或許很多人搞不懂,簡單說就是BIOS提供一些資訊給OS,提供的方式是透過ACPI Table(放在記憶體中),OS自己會去記憶體中找到ACPI Table,然後得到BIOS所提供的資訊,其中這些資訊就包含PM Register的IO Address。
底下提供一個範例程式來做系統關機的動作,如果你是在DOS下可直接用Debug.com 執行
如果是在 Windows下請自行撰寫Ring 0 程式去對IO Access 或利用相關工具:
mov ax,3c00h <--請參考ICH 手冊上面的說明
mov dx,0404h <--PM Register I/O Address. (PMBase+04h)
out dx,ax
上面有提到PM Register IO Address 資訊是由BIOS所提供,因此不同的BIOS廠商所設定 PM Register 的IO Address range也會不同,所以如果想要強制關機就必須先找到這個位址,這樣才能對PM Register做Soft off (S5 satte)的動作。 一般比較常見的就是Port 0404h,所以如果你不知道BIOS工程師設定在哪個位址,那麼你可以利用ACPI Table查詢或是請教你們的BIOS工程師。
另外如果你自己在Windows下寫了一個Ring 0 層級的程式去做S5 state(Soft Off),你會發現系統會直接關機,也就是OS不會去通知Driver工作站要關機,而是直接(立即)由硬體那邊關閉系統。
Reference
ACPI Spec 3.0
ICH8 Spec
一般在NoteBook上面控制電源的部分都是EC BIOS所控制,而有時候我們會透過Send EC Cmd方式給EC BIOS來達到關機的目的(Power Off)。
像是某些BIOS中會使用到關機的地方就是在Setup Menu-->Exit-->Shutdown 選項,而這個地方的BIOS code 就可以利用EC Cmd或是PM Register來達到關機的目的。
另外在進入作業系統後我們也可以透過上面的方式來強制關閉系統,所使用的方式就是利用PM Control Register。
所以BIOS扮演的角色就是去設定PM Register IO Address Range & ACPI Table ,這樣說或許很多人搞不懂,簡單說就是BIOS提供一些資訊給OS,提供的方式是透過ACPI Table(放在記憶體中),OS自己會去記憶體中找到ACPI Table,然後得到BIOS所提供的資訊,其中這些資訊就包含PM Register的IO Address。
底下提供一個範例程式來做系統關機的動作,如果你是在DOS下可直接用Debug.com 執行
如果是在 Windows下請自行撰寫Ring 0 程式去對IO Access 或利用相關工具:
mov ax,3c00h <--請參考ICH 手冊上面的說明
mov dx,0404h <--PM Register I/O Address. (PMBase+04h)
out dx,ax
上面有提到PM Register IO Address 資訊是由BIOS所提供,因此不同的BIOS廠商所設定 PM Register 的IO Address range也會不同,所以如果想要強制關機就必須先找到這個位址,這樣才能對PM Register做Soft off (S5 satte)的動作。 一般比較常見的就是Port 0404h,所以如果你不知道BIOS工程師設定在哪個位址,那麼你可以利用ACPI Table查詢或是請教你們的BIOS工程師。
另外如果你自己在Windows下寫了一個Ring 0 層級的程式去做S5 state(Soft Off),你會發現系統會直接關機,也就是OS不會去通知Driver工作站要關機,而是直接(立即)由硬體那邊關閉系統。
Reference
ACPI Spec 3.0
ICH8 Spec
標籤:
IA32 相關基礎知識
星期一, 7月 02, 2007
逆向工程-基礎知識
MASM 運算式 C++ 運算式
========================================
BYTE PTR [bx] *(unsigned char) ebx
WORD PTR [bp] *(unsigned short *) ebp
DWORD PTR [bp] *(unsigned long *) ebp
OFFSET Var &Var
;----------------------------
; 呼叫協定 test1(par1,par2,par3)
;----------------------------
__cdecl PASCAL __stdcall
push par3; push par1 push par3
push par2; push par2 push par2
push par1; push par3 push par1
call test1 call test1 call test1;//函數內回復堆疊
add esp,0ch //回復堆疊
由上面範例可以得知,_cdecl 與_stdcall的參數都是由右邊開始堆到堆疊去,也就是說par1會在堆疊頂端。
依照__stdcall 範例:
push par2
push par1
call test2;
{
push ebp //保留原來的ebp
mov ebp,esp //ebp指向目前的堆疊頂端
mov eax, [ebp+0c] //參數par2
mov ebx, [ebp+08] //參數par1
sub esp,8 //如果程式內有變數,則保留變數大小,例如int i,j
...
add esp,8 //釋放變數佔用的堆疊
pop ebp //復原ebp
ret 8 //相當於ret : add esp,8
}
;----------------------------------------------------
;堆疊示意圖 (堆疊位址是由高往下,也就是堆疊頂端位址是低位址)
;----------------------------------------------------
... <-- ESP 會一直指向堆疊的最頂端
程式內的變數j <-- EBP-8h
程式內的變數i <-- EBP-4h
保留的EBP <-- EBP + 0 (進入副程式後會mov ebp,esp)
返回位址(Offset) <-- EBP + 4h
Par1 <-- EBP + 8h
Par2 <-- EBP + 0Ch (由左至右堆入堆疊,所以第一個被堆進去)
...
========================================
BYTE PTR [bx] *(unsigned char) ebx
WORD PTR [bp] *(unsigned short *) ebp
DWORD PTR [bp] *(unsigned long *) ebp
OFFSET Var &Var
;----------------------------
; 呼叫協定 test1(par1,par2,par3)
;----------------------------
__cdecl PASCAL __stdcall
push par3; push par1 push par3
push par2; push par2 push par2
push par1; push par3 push par1
call test1 call test1 call test1;//函數內回復堆疊
add esp,0ch //回復堆疊
由上面範例可以得知,_cdecl 與_stdcall的參數都是由右邊開始堆到堆疊去,也就是說par1會在堆疊頂端。
依照__stdcall 範例:
push par2
push par1
call test2;
{
push ebp //保留原來的ebp
mov ebp,esp //ebp指向目前的堆疊頂端
mov eax, [ebp+0c] //參數par2
mov ebx, [ebp+08] //參數par1
sub esp,8 //如果程式內有變數,則保留變數大小,例如int i,j
...
add esp,8 //釋放變數佔用的堆疊
pop ebp //復原ebp
ret 8 //相當於ret : add esp,8
}
;----------------------------------------------------
;堆疊示意圖 (堆疊位址是由高往下,也就是堆疊頂端位址是低位址)
;----------------------------------------------------
... <-- ESP 會一直指向堆疊的最頂端
程式內的變數j <-- EBP-8h
程式內的變數i <-- EBP-4h
保留的EBP <-- EBP + 0 (進入副程式後會mov ebp,esp)
返回位址(Offset) <-- EBP + 4h
Par1 <-- EBP + 8h
Par2 <-- EBP + 0Ch (由左至右堆入堆疊,所以第一個被堆進去)
...
C語言複習~~~指標與函數
年紀大,東西都忘東忘西的,今天複習C Primer Plus 4/e,把一些東西記錄下來。
高度技巧的宣告
* 表示一個指標
() 表示一個函數
[] 表示一個陣列
※()與[]有相同的優先順序,他們的結合姓是由左到右 →
int a[8][8]; //int陣列的陣列
int **ptr; //指向int 指標的指標
int *risks[10]; //指向int 型態的10個指標陣列
int (*risks)[10]; //指向10個int 陣列的指標
int *oof[3][4]; //指向int型態的3x4指標陣列
int (*uuf)[3][4]; //指向3x4 int 陣列的指標
int (*uof[3])[4]; //指向4個元素的int 陣列的3個元素的指標陣列
char *fump(); //回傳指向字元的指標的函數
char (*fump)(); //指向會回傳字元型態的函數的指標
char (*fump[3])(); //3個指向會回傳字元型態的函數的指標陣列
typedef int arr5[5];
typedef arr5 *P_arr5;
typedef p_arr5 arrp10[10];
arr5 togs; //togs是有5個int元素的陣列
p_arr5 P2; //p2是指向有5個int陣列的指標
arrp10 ap; //ap是10個指向5個int陣列的指標陣列
函數宣告與指標
void ToUpper(char *); //將字串轉大寫
其中ToUpper()函數型態是void,參數是char *,如果宣告為這個函數宣告一個 pf指標,就會像下面一樣:
void (*pf)(char*); //指向函數的指標,例如: pf=ToUpper,但是不能寫成pf=ToUpper();
其中括號內的(*pf)會被括號結合,所以閱讀時他會是一個指標,所以他是一個指向函數的指標,如果省略就會不同意義:
void *pf(char *) ; //pf 是回傳指標的函數
※一般如果沒有註明時,定義的名稱本身就是位址起始,例如:
int a[10];
int *p;
p=a; 或是 p=&a[0];
void ToUpper(char *);
void (*pf)(char*);
pf=ToUpper;
應用範例:
void ToUpper(char *);
void (*pf)(char*);
void Show(void (*ff)(char*), char *str);
pf=ToUpper;
Show(ToUpper, mis); //Show函數使用ToUpper函數: ff=ToUpper
Show(pf, mis); //Show函數使用ToUpper函數: ff=pf
void Show(void (*ff)(char*), char *str)
{
(*ff)(str); //呼叫ToUpper函數,參數=str
puts(str); //顯示字串
}
C語言與組合語言對應
;-------------------------------------------
; VOID Funcation_A(VOID);
;-------------------------------------------
Funcation_A PROC PUBLIC
push eax
....
pop eax
ret
Funcation_A ENDP
;------------------------------------------------------------------------------
; VOID Funcation_B (IN UINT32 RegisterInEax, OUT OEM_REGISTER *Reg);
;
;typedef struct {
; UINT32 RegEax;
; UINT32 RegEbx;
; UINT32 RegEcx;
; UINT32 RegEdx;
;}OEM_REGISTER;
;------------------------------------------------------------------------------
Funcation_B PROC PUBLIC RegisterInEax:UINT32, Reg:PTR EFI_CPUID_REGISTER
pushad
...
mov edi, DWORD PTR Reg
...
mov DWORD PTR [edi].RegEax, eax ; Reg->RegEax
...
popad
ret
Funcation_B ENDP
;------------------------------------------------------------------------------
; UINT32 OemIoPortRead32 (IN UINT64 Address); //回傳值放EAX
;------------------------------------------------------------------------------
OemIoPortRead32 PROC PUBLIC Address:UINT64
push edx
mov edx, DWORD PTR Address
in eax, dx
pop edx
ret
OemIoPortRead32 ENDP
高度技巧的宣告
* 表示一個指標
() 表示一個函數
[] 表示一個陣列
※()與[]有相同的優先順序,他們的結合姓是由左到右 →
int a[8][8]; //int陣列的陣列
int **ptr; //指向int 指標的指標
int *risks[10]; //指向int 型態的10個指標陣列
int (*risks)[10]; //指向10個int 陣列的指標
int *oof[3][4]; //指向int型態的3x4指標陣列
int (*uuf)[3][4]; //指向3x4 int 陣列的指標
int (*uof[3])[4]; //指向4個元素的int 陣列的3個元素的指標陣列
char *fump(); //回傳指向字元的指標的函數
char (*fump)(); //指向會回傳字元型態的函數的指標
char (*fump[3])(); //3個指向會回傳字元型態的函數的指標陣列
typedef int arr5[5];
typedef arr5 *P_arr5;
typedef p_arr5 arrp10[10];
arr5 togs; //togs是有5個int元素的陣列
p_arr5 P2; //p2是指向有5個int陣列的指標
arrp10 ap; //ap是10個指向5個int陣列的指標陣列
函數宣告與指標
void ToUpper(char *); //將字串轉大寫
其中ToUpper()函數型態是void,參數是char *,如果宣告為這個函數宣告一個 pf指標,就會像下面一樣:
void (*pf)(char*); //指向函數的指標,例如: pf=ToUpper,但是不能寫成pf=ToUpper();
其中括號內的(*pf)會被括號結合,所以閱讀時他會是一個指標,所以他是一個指向函數的指標,如果省略就會不同意義:
void *pf(char *) ; //pf 是回傳指標的函數
※一般如果沒有註明時,定義的名稱本身就是位址起始,例如:
int a[10];
int *p;
p=a; 或是 p=&a[0];
void ToUpper(char *);
void (*pf)(char*);
pf=ToUpper;
應用範例:
void ToUpper(char *);
void (*pf)(char*);
void Show(void (*ff)(char*), char *str);
pf=ToUpper;
Show(ToUpper, mis); //Show函數使用ToUpper函數: ff=ToUpper
Show(pf, mis); //Show函數使用ToUpper函數: ff=pf
void Show(void (*ff)(char*), char *str)
{
(*ff)(str); //呼叫ToUpper函數,參數=str
puts(str); //顯示字串
}
C語言與組合語言對應
;-------------------------------------------
; VOID Funcation_A(VOID);
;-------------------------------------------
Funcation_A PROC PUBLIC
push eax
....
pop eax
ret
Funcation_A ENDP
;------------------------------------------------------------------------------
; VOID Funcation_B (IN UINT32 RegisterInEax, OUT OEM_REGISTER *Reg);
;
;typedef struct {
; UINT32 RegEax;
; UINT32 RegEbx;
; UINT32 RegEcx;
; UINT32 RegEdx;
;}OEM_REGISTER;
;------------------------------------------------------------------------------
Funcation_B PROC PUBLIC RegisterInEax:UINT32, Reg:PTR EFI_CPUID_REGISTER
pushad
...
mov edi, DWORD PTR Reg
...
mov DWORD PTR [edi].RegEax, eax ; Reg->RegEax
...
popad
ret
Funcation_B ENDP
;------------------------------------------------------------------------------
; UINT32 OemIoPortRead32 (IN UINT64 Address); //回傳值放EAX
;------------------------------------------------------------------------------
OemIoPortRead32 PROC PUBLIC Address:UINT64
push edx
mov edx, DWORD PTR Address
in eax, dx
pop edx
ret
OemIoPortRead32 ENDP
星期四, 6月 21, 2007
S3 Resume
今天在測試EFI 的S3 Resume功能,看著C語言的寫法突然間把我原本在P廠商搞不清楚的問題都解決了,果然學BIOS還是從頭學會比較有感覺。
[學習紀錄]
1. ACPI Table的建立與放置的位址
2. 如何從F/E Segment 中找到ACPI Table
3. FACS中的Wake Vector 與OS、BIOS之間的互動
4. PM Register 與Sx State 的關係
[學習紀錄]
1. ACPI Table的建立與放置的位址
2. 如何從F/E Segment 中找到ACPI Table
3. FACS中的Wake Vector 與OS、BIOS之間的互動
4. PM Register 與Sx State 的關係
標籤:
IA32 相關基礎知識
星期二, 6月 19, 2007
星期四, 5月 31, 2007
EFI v.s #define
今天把EFI 的一些心得筆記整理的差不多了,所以開始去Trace 一些Source code,在追蹤的過程中發現了Intel 在寫EFI 的時候真的把所有的東西都模組化了,以後寫BIOS的時候可以很方便的去撰寫。
而在這些EFI Code之中,因為幾乎都是C (不是C++,但是Phoenix BIOS可以使用C++方式撰寫,不過我還沒接觸到他們的EFI,所以還不確定是否真的可以!)的語法去撰寫,所以在模組化的同時他也使用了很多巨集指令,也就是使用#define 的方式去宣告了很多不同的巨集指令,在這裡面發現了"##"這個東西,例如下面的範例:
#define MyMacro(a,b,c) {a ## b ##c}
MyMacro(A,B,C)
而##是用來連結字串的,所以展開後會變成ABC,所以在Blog 留下筆記心得的紀錄。
而在這些EFI Code之中,因為幾乎都是C (不是C++,但是Phoenix BIOS可以使用C++方式撰寫,不過我還沒接觸到他們的EFI,所以還不確定是否真的可以!)的語法去撰寫,所以在模組化的同時他也使用了很多巨集指令,也就是使用#define 的方式去宣告了很多不同的巨集指令,在這裡面發現了"##"這個東西,例如下面的範例:
#define MyMacro(a,b,c) {a ## b ##c}
MyMacro(A,B,C)
而##是用來連結字串的,所以展開後會變成ABC,所以在Blog 留下筆記心得的紀錄。
標籤:
EFI BIOS相關知識
星期二, 5月 29, 2007
EFI Protocol 觀念小筆記
C語言的宣告與定義:
1.C語言中只允許變數或函數的定義出現一次。
由於變數只有定義規則,沒有宣告規則,所以當你鍵入int a;時,代表此變數已經被定義了。我們如何宣告一個變數呢? 加上extern 變成exern int a;,告訴comipler,此變數a只是一個宣告,它的定義在別處。
2. C語言函數規則分成宣告與定義二種。當一個函數只有傳回值、名稱、傳入值, 沒有大括號{},compiler會把此種形式當成是函數宣告;如果加上了{}, 形成函數定義。所以你要宣告一個函數,只須鍵入int f(); compiler會自動把它示為函數宣告。
// EFI Protocol
------------------
EFI Protocol 可以看作是一個介面(Interface),透過這個Interface 去存取Data 或是函數,簡單說就是一個指標,指標指向的記憶體可能是Data,也可能是函數。
安裝一個 EFI Protocol 後會以 (GUID | Pointer) 的欄位格式存放在Handle DataBase中,然後就可以在其他地方利用LocateProtcol()方式去得到指標,然後使用他。
1.Data 型式的EFI Protocol
(a)宣告你的Sturcture與GUID在你的Protocol.h 與Protocol.c中
EFI_GUID gGuid=MY_GUID;
Extern gGuid;
typedef struct {
...
UINT8 Array[10];
...
} MY_PROTOCOL;
(b)在DXE Driver中使用使用MY_PROTOCOL "定義"一個變數Test
UINT16 i;
MY_PROTOCOL Test={......};
然後在呼叫InstallProtocol(&gGuid,&Test)去安裝一個Protocol到Handle Database,所以Handle Database中的Guid=gGuid,而指標Pointer=&Test。
2.Function 型式的EFI Protocol也是跟Data型式一樣作法,不同的地方在於宣告的是函數原型。
所以觀念在於如何去宣告Protocol與如何透過GUID+Pointer去做你要做的事情。
1.C語言中只允許變數或函數的定義出現一次。
由於變數只有定義規則,沒有宣告規則,所以當你鍵入int a;時,代表此變數已經被定義了。我們如何宣告一個變數呢? 加上extern 變成exern int a;,告訴comipler,此變數a只是一個宣告,它的定義在別處。
2. C語言函數規則分成宣告與定義二種。當一個函數只有傳回值、名稱、傳入值, 沒有大括號{},compiler會把此種形式當成是函數宣告;如果加上了{}, 形成函數定義。所以你要宣告一個函數,只須鍵入int f(); compiler會自動把它示為函數宣告。
// EFI Protocol
------------------
EFI Protocol 可以看作是一個介面(Interface),透過這個Interface 去存取Data 或是函數,簡單說就是一個指標,指標指向的記憶體可能是Data,也可能是函數。
安裝一個 EFI Protocol 後會以 (GUID | Pointer) 的欄位格式存放在Handle DataBase中,然後就可以在其他地方利用LocateProtcol()方式去得到指標,然後使用他。
1.Data 型式的EFI Protocol
(a)宣告你的Sturcture與GUID在你的Protocol.h 與Protocol.c中
EFI_GUID gGuid=MY_GUID;
Extern gGuid;
typedef struct {
...
UINT8 Array[10];
...
} MY_PROTOCOL;
(b)在DXE Driver中使用使用MY_PROTOCOL "定義"一個變數Test
UINT16 i;
MY_PROTOCOL Test={......};
然後在呼叫InstallProtocol(&gGuid,&Test)去安裝一個Protocol到Handle Database,所以Handle Database中的Guid=gGuid,而指標Pointer=&Test。
2.Function 型式的EFI Protocol也是跟Data型式一樣作法,不同的地方在於宣告的是函數原型。
所以觀念在於如何去宣告Protocol與如何透過GUID+Pointer去做你要做的事情。
標籤:
EFI BIOS相關知識
星期一, 5月 28, 2007
今天收到的新聞~~~
外傳廣達接獲新的iPhone訂單,廣達表示,廣達接單方向積極朝多元化產品發展,不會對單一客戶進行評論。據瞭解,開發一款手機約要六個月到九個月,隨著第一代iPhone推出,蘋果內部確實也在開發第二代iPhone,但訂單花落誰家,要視第一代產品銷售狀況才會決定。
看樣子我要去問問我那些做Apple代工的朋友們消息是否正確了....呵呵!
看樣子我要去問問我那些做Apple代工的朋友們消息是否正確了....呵呵!
星期三, 5月 09, 2007
Direct Hardware Access Under Windows 9x/NT/2000/XP/Vista
今天又多學一樣東西,就是呼叫Undocument API 去開啟I/O 存取權限,這樣子你就可以使用Ring3的應用程式直接存取硬體,例如下面的VC++範例程式中直接使用Assembly 或是IO Funcation call去存取硬體。
EX1: Read I/O Port
_asm {
pushf
mov al, ireg.h
mov dx,0x70
out dx,al
mov dx,0x71
in al, dx
mov mbid, al
popf
}
EX2: Read CMOS
_outp(0x70,ireg.h );
mbid=_inp(0x71);
使用的技巧是先進入Ring0 (也就是掛一個Driver),然後在DriverEntry 時去呼叫API去改變IO權限。
void Ke386SetIoAccessMap(int, IOPM *);
void Ke386QueryIoAccessMap(int, IOPM *);
void Ke386IoSetAccessProcess(PEPROCESS, int);
開剛始是利用修改TSS的來開啟IO存取權限方式,不過改完後DEBUG.COM會無法執行,所以才改成呼叫上面的API的方式,而直接修改TSS方式如下所示:
__asm {
cli // 遮罩中斷
sgdt gdtr // 得到 GDT 基底位址與段界限
str TSSseg // 得到 TSS 選擇子
movzx esi,TSSseg // 擴展到 ESI 中以便計算
add esi,gdtr.dwBase // 得到 TSS 在 GDT 中描述符
mov gdt,esi
}
不管是API還是修改TSS,都是在DriverEntery進去後去執行的。
因此實作部份分成三部份:
1.WDM Driver code(DriverEntery 的地方呼叫API)
2.InstallDriver code(應用程式執行時去動態載入驅動程式)
3.應用程式Application code(直接使用Assembly或是IO Function)
EX1: Read I/O Port
_asm {
pushf
mov al, ireg.h
mov dx,0x70
out dx,al
mov dx,0x71
in al, dx
mov mbid, al
popf
}
EX2: Read CMOS
_outp(0x70,ireg.h );
mbid=_inp(0x71);
使用的技巧是先進入Ring0 (也就是掛一個Driver),然後在DriverEntry 時去呼叫API去改變IO權限。
void Ke386SetIoAccessMap(int, IOPM *);
void Ke386QueryIoAccessMap(int, IOPM *);
void Ke386IoSetAccessProcess(PEPROCESS, int);
開剛始是利用修改TSS的來開啟IO存取權限方式,不過改完後DEBUG.COM會無法執行,所以才改成呼叫上面的API的方式,而直接修改TSS方式如下所示:
__asm {
cli // 遮罩中斷
sgdt gdtr // 得到 GDT 基底位址與段界限
str TSSseg // 得到 TSS 選擇子
movzx esi,TSSseg // 擴展到 ESI 中以便計算
add esi,gdtr.dwBase // 得到 TSS 在 GDT 中描述符
mov gdt,esi
}
不管是API還是修改TSS,都是在DriverEntery進去後去執行的。
因此實作部份分成三部份:
1.WDM Driver code(DriverEntery 的地方呼叫API)
2.InstallDriver code(應用程式執行時去動態載入驅動程式)
3.應用程式Application code(直接使用Assembly或是IO Function)
標籤:
Windows 程式相關
星期二, 5月 08, 2007
Windows 下的I/O存取
昨天我們Leader 請我幫忙寫一個Tools給對岸同胞的生產線使用,他們需要在Vista 底下去存取Panel ID.
所以自己留下個撰寫紀錄,讓自己下次修改自己的Source Code的時候有個依據。
目前Panel ID我們是使用跳線的方式去改變GPIO值來做辨別,所以只要去存取GPIO就可以了。
1.南橋ICH8會把GPIO 值 mapped 到某個IO Range...所以只要利用組合語言的IN/OUT 去存取他映對過去的IO 位址就可以知道GPIO的值。
2.查看主機板電路圖,看實際上被當成選擇Panel ID的GPIO是哪幾支接腳。
3.開發一個DOS版的Tools直接使用IN/OUT指令去存取,例如下面範例:
byte ReadGpioStatus(word OEM_GPIO_ADDR)
{
byte mbid;
...
_asm {
mov dx, OEM_GPIO_ADDR
in ax, dx
mov mbid, al
}
...
return mbid;
}
4.開發一個Windows 版的Tools也是使用IN/OUT指令,不過是由自己撰寫的I/O Driver所提供
byte ReadGpioStatus(word OEM_GPIO_ADDR)
{
byte mbid;
mbid=IN_B(OEM_GPIO_ADDR); //IN_B 會去呼叫Windows API然後送IRP給我自己寫的Driver
return mbid;
}
5.在Source code內做一個介面IN_B() 然後呼叫API去使用自己寫的I/O Driver所提供的服務
byte IN_B (WORD port)
{
ireg.x.DX = port;
...
bRc = DeviceIoControl (...); //送IRP給我自己寫的I/O Driver...
...
return (ireg.h.AL);
}
6.整個Windows版本的Source code架構與DOS版本差異不大,只是Windows版本需要去動態載入驅動程式,而這部分寫在if (!InitializeWinIo()) return 1 ;
上述這幾個部份是比較重要的地方,而整個DOS版與Windows版的程式架構如下:
int main(int argc,char *argv[])
{
modelString[]={
"000"," Reserved. ",
"001"," Vendor1. ",
"002"," Vendor2. ",
....
}
...
if (!InitializeWinIo()) return 1 ; // For Windows version only.
...
pStatus1=ReadGpioStatus(OEM_GPIO_DEFAULT_IO1); // Read Gpio
pStatus2=ReadGpioStatus(OEM_GPIO_DEFAULT_IO2);
...
panelid=(GetBit(pStatus1,2)<<2)+(GetBit(pStatus1,6)<<1)+GetBit(pStatus2,6);
...
if (strcmp(argv[2], modelString[panelid*2]))
{
...// if match...return passed!
}
return 0;
}
從上面範例可以知道其實DOS版與Windows版的Source code 寫法幾乎一樣,只是Windows版因為無法直接存取I/O,所以我自己多加了一個I/O Driver,然後做了兩個Function (IN_B()跟InitializeWinIo())來載入與使用這個Driver 。
所以自己留下個撰寫紀錄,讓自己下次修改自己的Source Code的時候有個依據。
目前Panel ID我們是使用跳線的方式去改變GPIO值來做辨別,所以只要去存取GPIO就可以了。
1.南橋ICH8會把GPIO 值 mapped 到某個IO Range...所以只要利用組合語言的IN/OUT 去存取他映對過去的IO 位址就可以知道GPIO的值。
2.查看主機板電路圖,看實際上被當成選擇Panel ID的GPIO是哪幾支接腳。
3.開發一個DOS版的Tools直接使用IN/OUT指令去存取,例如下面範例:
byte ReadGpioStatus(word OEM_GPIO_ADDR)
{
byte mbid;
...
_asm {
mov dx, OEM_GPIO_ADDR
in ax, dx
mov mbid, al
}
...
return mbid;
}
4.開發一個Windows 版的Tools也是使用IN/OUT指令,不過是由自己撰寫的I/O Driver所提供
byte ReadGpioStatus(word OEM_GPIO_ADDR)
{
byte mbid;
mbid=IN_B(OEM_GPIO_ADDR); //IN_B 會去呼叫Windows API然後送IRP給我自己寫的Driver
return mbid;
}
5.在Source code內做一個介面IN_B() 然後呼叫API去使用自己寫的I/O Driver所提供的服務
byte IN_B (WORD port)
{
ireg.x.DX = port;
...
bRc = DeviceIoControl (...); //送IRP給我自己寫的I/O Driver...
...
return (ireg.h.AL);
}
6.整個Windows版本的Source code架構與DOS版本差異不大,只是Windows版本需要去動態載入驅動程式,而這部分寫在if (!InitializeWinIo()) return 1 ;
上述這幾個部份是比較重要的地方,而整個DOS版與Windows版的程式架構如下:
int main(int argc,char *argv[])
{
modelString[]={
"000"," Reserved. ",
"001"," Vendor1. ",
"002"," Vendor2. ",
....
}
...
if (!InitializeWinIo()) return 1 ; // For Windows version only.
...
pStatus1=ReadGpioStatus(OEM_GPIO_DEFAULT_IO1); // Read Gpio
pStatus2=ReadGpioStatus(OEM_GPIO_DEFAULT_IO2);
...
panelid=(GetBit(pStatus1,2)<<2)+(GetBit(pStatus1,6)<<1)+GetBit(pStatus2,6);
...
if (strcmp(argv[2], modelString[panelid*2]))
{
...// if match...return passed!
}
return 0;
}
從上面範例可以知道其實DOS版與Windows版的Source code 寫法幾乎一樣,只是Windows版因為無法直接存取I/O,所以我自己多加了一個I/O Driver,然後做了兩個Function (IN_B()跟InitializeWinIo())來載入與使用這個Driver 。
標籤:
Windows 程式相關
星期一, 5月 07, 2007
SEGMENT與Public/Extern 用法
在BIOS Source code之中常常會看到一大堆的Segment,而有時候相同的Segment內去使用同一個Segment內的程序時,會在Segment 內去把他Extern進來,如下範例所示:
Test1.asm
AAA SEGMENT <--AAA Segment 裡面有一個BBB程序被共用(Public)
....
BBB PROC NEAR PUBLIC
...
BBB ENDP
....
AAA ENDS
Test2.asm內容如下
AAA SEGMENT <--AAA Segment內有一個CCC程序要去呼叫BBB程序,但是要在AAA Segment把BBB程式用Extern方式拉進來
EXTERN BBB:NEAR
....
CCC PROC
...
CALL BBB
...
CCC ENDP
AAA ENDS
Public 是把程序共用,而Extern 可看作要使用一個共用的程序
一般來說如果BBB程序與CCC程序都在同一個檔案內,那麼CCC程序可以直接呼叫BBB程序而不需要Extern
而不同"檔案"時就要這樣做,如上面範例是Test1.asm 跟Test2.asm
Test1.asm
AAA SEGMENT <--AAA Segment 裡面有一個BBB程序被共用(Public)
....
BBB PROC NEAR PUBLIC
...
BBB ENDP
....
AAA ENDS
Test2.asm內容如下
AAA SEGMENT <--AAA Segment內有一個CCC程序要去呼叫BBB程序,但是要在AAA Segment把BBB程式用Extern方式拉進來
EXTERN BBB:NEAR
....
CCC PROC
...
CALL BBB
...
CCC ENDP
AAA ENDS
Public 是把程序共用,而Extern 可看作要使用一個共用的程序
一般來說如果BBB程序與CCC程序都在同一個檔案內,那麼CCC程序可以直接呼叫BBB程序而不需要Extern
而不同"檔案"時就要這樣做,如上面範例是Test1.asm 跟Test2.asm
標籤:
組合語言Assembly
Strong/Weak 語法與"::"標籤表示法
組合語言中的Strong/Weak 語法簡單說就是如果A存在就跳躍過去A執行,如果A不存在就跳到B去執行
假如我有一段程式如下面所示:
PUBLIC BBB&Return
EXTERN AAA(BBB&Return):NEAR <--Strong/weak寫法
Test1 Proc
...
Jmp AAA
...
BBB&Return:: <--如果AAA不存在會跳到這邊執行
ret
Test1 EndP
AAA Proc <--如果AAA存在會跳到這邊執行
...
ret
AAA EndP
除了上面的Strong/Weak之外,還可以看到標籤是BBB&Return:: 而"::"代表的意思有兩種:
1.PUBLIC 出來的標籤,要把它標示成"::",簡單說就是不同程序間也可以跳躍
2."::"不同程序間的跳躍,如下面範例所示:
[註]功能還是跟 ":"一樣,只是表示方式不同而已。
Public Label2
Test1 proc near
Label1:
Label2::
ret
Test1 endp
Test2 proc near
jmp Label1 ; compiler 會錯誤, 因為該 label 找不到
jmp Label2 ; 正確, 該 label2 是可以找到的,因為有把他Public
ret
Test2 endp
假如我有一段程式如下面所示:
PUBLIC BBB&Return
EXTERN AAA(BBB&Return):NEAR <--Strong/weak寫法
Test1 Proc
...
Jmp AAA
...
BBB&Return:: <--如果AAA不存在會跳到這邊執行
ret
Test1 EndP
AAA Proc <--如果AAA存在會跳到這邊執行
...
ret
AAA EndP
除了上面的Strong/Weak之外,還可以看到標籤是BBB&Return:: 而"::"代表的意思有兩種:
1.PUBLIC 出來的標籤,要把它標示成"::",簡單說就是不同程序間也可以跳躍
2."::"不同程序間的跳躍,如下面範例所示:
[註]功能還是跟 ":"一樣,只是表示方式不同而已。
Public Label2
Test1 proc near
Label1:
Label2::
ret
Test1 endp
Test2 proc near
jmp Label1 ; compiler 會錯誤, 因為該 label 找不到
jmp Label2 ; 正確, 該 label2 是可以找到的,因為有把他Public
ret
Test2 endp
標籤:
組合語言Assembly
星期日, 5月 06, 2007
BIOS Code Entry point
前一篇說明了CPU如何去讀取第一條存在於BIOS ROM裡面的程式碼,現在就說明一下實際上BIOS Code是怎樣跑的。
這邊要重新說明一下觀念:
FFFF_FFF0 是第一條指令所存放的地方,而他是在4G頂端的地方,CPU在被重置(Reset)之後會去這邊讀取指令,而對CPU來說並不會在意Address FFFF_FFF0 是給BIOS ROM使用還是記憶體DRAM使用,而80386之後,這個位址是都給BIOS ROM使用。
000F_FFF0 (或表示成F000:FFF0),這個位址是在1M 頂端的地方,而這個位址是給BIOS ROM還是DRAM?
給誰使用則是去設定北橋,一般來說在POST 前(Power On-->BootBlock),這個地方應該是給BIOS ROM使用,而POST中途會去做Shadow動作,這個動作做完後,這個位址會給DRAM使用。
以前8088/8086 在1M頂端是給BIOS ROM使用,後來才變成BIOS ROM/DRAM 混用,所以以前CPU被重置的時候是去F_FFF0(F000:FFF0)讀取第一條指令,後來的CPU就不這樣做了。
上面觀念說明完之後我們就實際追蹤一下BIOS code的流程;在進入到OS之後,如果你去看000F_FFF0的資料,其實是看到DRAM裡面的資料,而這個資料是BIOS放進去的;而進入OS之前,這個位址的資料因為預設是給BIOS ROM使用,所以會跟FFFF_FFF0看到的資料一模一樣。
下面是實際上我們去追蹤P廠商BIOS code的範例:
1.我們先利用一些工具去在OS中去存取4G頂端的位址FFFF_FFF0 看到的資料如下所示:
FFFF_FFF0 E9 55 B2 ........... -->實際在BIOS ROM裡面的資料,因為這個位址是給BIOS ROM使用
上面看到的E9 55 B2 就是CPU讀取的第一條指令,而這個指令是一個Jmp 指令,他會跳到BootBlock Entry Point去執行,執行完畢後才會跳到POST Entry point.
2.下面也是在OS中利用工具去讀取1M頂端的資料,由於已經進入OS了,所以這些資料是BIOS填進去DRAM的資料(Shadow),也就是說這個000F_FFF0是被指向DRAM之中。
F000:FFF0 EA 5B E0 00 F0 ..... -->跳到F000:E05B
F000:E05B E9 FB A8 ........... -->跳到實際的POST Entry point....
A8FB+E05E=8959 (進位1 去掉)
F000:8959 FA ................. -->FA=CLI
這邊可以看到為了相容性問題,所以000F_FFF0是放著Jmp F000:E05B 位址,而這個位址裡面會放著POST Entry point指標(E9 FB A8),所以繼續追蹤過去後可以發現POST的第一條指令是CLI...
3.其實CPU第一條指令E9 55 B2 會跳到BootBlock去執行;而執行完畢後,BootBlock也會跳到跟F000:8959內一樣的BIOS code去執行所謂的POST。
所以簡單的描述上面所說的流程:
1.CPU去FFFF_FFF0 提取第一條指令執行(Jmp BootBlock)
2.執行BootBlock
3.執行完bootBlock後,跳到POST
4.執行POST (POST第一條指令一定是CLI)
5.POST過程中Shadow ,Shadow後000F_FFF0的資料就是Jmp F000:E05B
這樣子說應該很容易瞭解了吧! ^^Y
這邊要重新說明一下觀念:
FFFF_FFF0 是第一條指令所存放的地方,而他是在4G頂端的地方,CPU在被重置(Reset)之後會去這邊讀取指令,而對CPU來說並不會在意Address FFFF_FFF0 是給BIOS ROM使用還是記憶體DRAM使用,而80386之後,這個位址是都給BIOS ROM使用。
000F_FFF0 (或表示成F000:FFF0),這個位址是在1M 頂端的地方,而這個位址是給BIOS ROM還是DRAM?
給誰使用則是去設定北橋,一般來說在POST 前(Power On-->BootBlock),這個地方應該是給BIOS ROM使用,而POST中途會去做Shadow動作,這個動作做完後,這個位址會給DRAM使用。
以前8088/8086 在1M頂端是給BIOS ROM使用,後來才變成BIOS ROM/DRAM 混用,所以以前CPU被重置的時候是去F_FFF0(F000:FFF0)讀取第一條指令,後來的CPU就不這樣做了。
上面觀念說明完之後我們就實際追蹤一下BIOS code的流程;在進入到OS之後,如果你去看000F_FFF0的資料,其實是看到DRAM裡面的資料,而這個資料是BIOS放進去的;而進入OS之前,這個位址的資料因為預設是給BIOS ROM使用,所以會跟FFFF_FFF0看到的資料一模一樣。
下面是實際上我們去追蹤P廠商BIOS code的範例:
1.我們先利用一些工具去在OS中去存取4G頂端的位址FFFF_FFF0 看到的資料如下所示:
FFFF_FFF0 E9 55 B2 ........... -->實際在BIOS ROM裡面的資料,因為這個位址是給BIOS ROM使用
上面看到的E9 55 B2 就是CPU讀取的第一條指令,而這個指令是一個Jmp 指令,他會跳到BootBlock Entry Point去執行,執行完畢後才會跳到POST Entry point.
2.下面也是在OS中利用工具去讀取1M頂端的資料,由於已經進入OS了,所以這些資料是BIOS填進去DRAM的資料(Shadow),也就是說這個000F_FFF0是被指向DRAM之中。
F000:FFF0 EA 5B E0 00 F0 ..... -->跳到F000:E05B
F000:E05B E9 FB A8 ........... -->跳到實際的POST Entry point....
A8FB+E05E=8959 (進位1 去掉)
F000:8959 FA ................. -->FA=CLI
這邊可以看到為了相容性問題,所以000F_FFF0是放著Jmp F000:E05B 位址,而這個位址裡面會放著POST Entry point指標(E9 FB A8),所以繼續追蹤過去後可以發現POST的第一條指令是CLI...
3.其實CPU第一條指令E9 55 B2 會跳到BootBlock去執行;而執行完畢後,BootBlock也會跳到跟F000:8959內一樣的BIOS code去執行所謂的POST。
所以簡單的描述上面所說的流程:
1.CPU去FFFF_FFF0 提取第一條指令執行(Jmp BootBlock)
2.執行BootBlock
3.執行完bootBlock後,跳到POST
4.執行POST (POST第一條指令一定是CLI)
5.POST過程中Shadow ,Shadow後000F_FFF0的資料就是Jmp F000:E05B
這樣子說應該很容易瞭解了吧! ^^Y
標籤:
IA32 相關基礎知識
星期日, 4月 29, 2007
x86 Intel CPU 的第一條指令~~~
一般CPU reset 後, 會預設跳躍至 0xffff_fff0 位址去執行, 這個位址是現行 BIOS ROM 映射的位址。而CPU被重置後的暫存器值為:
CS= F000h
EIP = 0000_FFF0h
Base Address= FFFF_0000h (隱含暫存器,不可見)
由於IA32 手冊中說明,IA32 CPU定址方式是利用Base Address+EIP,所以第一條指令讀取的位址是: Base Addr+EIP=FFFF_FFF0h,至於為什麼不是CS:IP的定址方式,則請參考IA32 手冊說明。
一般的做法是把 bootblock 的source code 放在這個區塊, 但是並不表示這個位址的 code 一定是 bootblock. 簡單說就是BIOS要不要做這一部分的功能就由你或是BIOS供應商決定.
一般而言,若有包 bootblock,則先跑 bootblock ; 之後才跳到 BIOS entry point ; 若沒包則直接跳到 BIOS entry point ( 這邊說的BIOS entry point 就是POST code一開始的地方,而他的第一條指令都是 cli ,而他的位址是固定的,也就是F000:E05B) !
Power on時 CPU 第一個 code read 一定是 0xFFFF_FFF0 ; 在那邊有放一個 jmp 指令 (因為 FFFFFFF0~FFFFFFFFh 只有 16-byte空間,無法放很多 code),如某個BIOS 在那個0xFFFF_FFF0的data 如下所示:
FFFFFFF0: E9 D3 E9 00 00 00 FE FF EA 39 EA ......
CPU讀取到E9 D3 E9 然後依照這個指令的描述 "轉跳" 到其他的地方 ,其中E9 是跳躍指令,jmp(E9)後面接的operand是相對位置,所以E9 "跳躍的位址"的計算方式:
"把現在的IP(*)加上operand就是target的IP "
0xFFF3 + 0xE9D3 = 0xE9C6(扣掉進位1,因為64k limit,範圍0000~FFFFh,會裁掉超出的部份)
(*當這到指令被CPU抓取時,IP就變成下一個指令的,所以目前的IP=0xFFF3);
若這 "跳躍的位址" 地方放的是 bootblock code,則先跑 bootblock。
所以由上面的範例可以知道,CPU讀取到第一行指令後會Jmp E9C6。
如果你是使用P廠商的BIOS,那麼這個位址就是指向BooBlock區塊的Entry point,因此可以證明他是有包BootBlock。
另外假設你的BIOS ROM大小為1MB ,那麼他會被映射到4G~(4G-1M)跟1M~640K的某幾個64K 的區域,簡單說就是4G~4G-1M 剛好就是對應到整顆BIOS ROM,而1M~640K則是某幾個64K會對應到BIOS ROM,例如E000h Segment(64K)或是F000h Segment(64k)=BIOS Data area,至於是誰要被映射過去就要看北橋的暫存器內的設定。
[註] 此篇文章整理自己在程式設計俱樂部的討論結果整理,感謝一些前輩指導。
[註] 如果要查看BIOS ROM的內容方式有下列幾種方式可以參考:
1.利用Debug dump 方式:利用OS自帶的Debug工具去將BIOS ROM資料讀到某個空閒的記憶體內查看
a. 在C:\> 鍵入 debug xxx.rom (以 256KB(3FFFFh)大小的BIOS ROM為例)
b. 在Debug提示"-"下鍵入"L 4000:0",其中L為Load意思,載入xxx.rom到記憶體4000:0的地方
所以40000~7FFFFh處為 xxx.rom的所有內容
c. 在Debug提示"-"下鍵入"D 7000:FFF0",其中D意思為Dump (查看記憶體資料)
此時會出現 7000:FFF0處的記憶體資料(也就是你載入的BIOS ROM資料)
2.若BIOS ROM size > 256KB,則使用 ultraedit 來看 ROM內容
3. 還可以使用反組譯工具Win32Dasm 或是IDA pro...等工具查看。
CS= F000h
EIP = 0000_FFF0h
Base Address= FFFF_0000h (隱含暫存器,不可見)
由於IA32 手冊中說明,IA32 CPU定址方式是利用Base Address+EIP,所以第一條指令讀取的位址是: Base Addr+EIP=FFFF_FFF0h,至於為什麼不是CS:IP的定址方式,則請參考IA32 手冊說明。
一般的做法是把 bootblock 的source code 放在這個區塊, 但是並不表示這個位址的 code 一定是 bootblock. 簡單說就是BIOS要不要做這一部分的功能就由你或是BIOS供應商決定.
一般而言,若有包 bootblock,則先跑 bootblock ; 之後才跳到 BIOS entry point ; 若沒包則直接跳到 BIOS entry point ( 這邊說的BIOS entry point 就是POST code一開始的地方,而他的第一條指令都是 cli ,而他的位址是固定的,也就是F000:E05B) !
Power on時 CPU 第一個 code read 一定是 0xFFFF_FFF0 ; 在那邊有放一個 jmp 指令 (因為 FFFFFFF0~FFFFFFFFh 只有 16-byte空間,無法放很多 code),如某個BIOS 在那個0xFFFF_FFF0的data 如下所示:
FFFFFFF0: E9 D3 E9 00 00 00 FE FF EA 39 EA ......
CPU讀取到E9 D3 E9 然後依照這個指令的描述 "轉跳" 到其他的地方 ,其中E9 是跳躍指令,jmp(E9)後面接的operand是相對位置,所以E9 "跳躍的位址"的計算方式:
"把現在的IP(*)加上operand就是target的IP "
0xFFF3 + 0xE9D3 = 0xE9C6(扣掉進位1,因為64k limit,範圍0000~FFFFh,會裁掉超出的部份)
(*當這到指令被CPU抓取時,IP就變成下一個指令的,所以目前的IP=0xFFF3);
若這 "跳躍的位址" 地方放的是 bootblock code,則先跑 bootblock。
所以由上面的範例可以知道,CPU讀取到第一行指令後會Jmp E9C6。
如果你是使用P廠商的BIOS,那麼這個位址就是指向BooBlock區塊的Entry point,因此可以證明他是有包BootBlock。
另外假設你的BIOS ROM大小為1MB ,那麼他會被映射到4G~(4G-1M)跟1M~640K的某幾個64K 的區域,簡單說就是4G~4G-1M 剛好就是對應到整顆BIOS ROM,而1M~640K則是某幾個64K會對應到BIOS ROM,例如E000h Segment(64K)或是F000h Segment(64k)=BIOS Data area,至於是誰要被映射過去就要看北橋的暫存器內的設定。
[註] 此篇文章整理自己在程式設計俱樂部的討論結果整理,感謝一些前輩指導。
[註] 如果要查看BIOS ROM的內容方式有下列幾種方式可以參考:
1.利用Debug dump 方式:利用OS自帶的Debug工具去將BIOS ROM資料讀到某個空閒的記憶體內查看
a. 在C:\> 鍵入 debug xxx.rom (以 256KB(3FFFFh)大小的BIOS ROM為例)
b. 在Debug提示"-"下鍵入"L 4000:0",其中L為Load意思,載入xxx.rom到記憶體4000:0的地方
所以40000~7FFFFh處為 xxx.rom的所有內容
c. 在Debug提示"-"下鍵入"D 7000:FFF0",其中D意思為Dump (查看記憶體資料)
此時會出現 7000:FFF0處的記憶體資料(也就是你載入的BIOS ROM資料)
2.若BIOS ROM size > 256KB,則使用 ultraedit 來看 ROM內容
3. 還可以使用反組譯工具Win32Dasm 或是IDA pro...等工具查看。
標籤:
IA32 相關基礎知識
訂閱:
文章 (Atom)