當前位置:首頁 » 編程軟體 » dynamic反編譯

dynamic反編譯

發布時間: 2022-05-18 14:22:28

1. C# AssemblyBuilder 動態生成DLL 生成出的類 怎麼加partial修飾符

partial類只存在於源代碼中,編譯之後就合並到一起了。所以你即使反編譯,也看不到partial。

2. 如何動態創建一個泛型類型的對象

使用Emit技術可以實現動態創建泛型或非泛型類型:

(){
AppDomainmyDomain=AppDomain.CurrentDomain;
varam=newAssemblyName("Dynamic");
//定義動態程序集
AssemblyBuilderab=myDomain.DefineDynamicAssembly(am,AssemblyBuilderAccess.RunAndSave);
//定義動態模塊
MoleBuildermb=ab.DefineDynamicMole("Dynamic",am.Name+".dll");
//定義類型
TypeBuildertb=mb.DefineType("App",TypeAttributes.Public);
//定義泛型類型參數
GenericTypeParameterBuilder[]typeParams=tb.DefineGenericParameters("T");
GenericTypeParameterBuilderT=typeParams[0];
//設置泛型類型約束
T.SetGenericParameterAttributes(GenericParameterAttributes.DefaultConstructorConstraint|GenericParameterAttributes.ReferenceTypeConstraint);
//構造私有欄位
FieldBuilderfb=tb.DefineField("_foo",T,FieldAttributes.Private);
MethodBuildermethod=tb.DefineMethod("Bar",MethodAttributes.Public);
//直接使用IL指令流進行封送
ILGeneratorilGen=method.GetILGenerator();//獲取方法的MSIL中間語言生成器
ilGen.Emit(OpCodes.Ldarg_0);//向IL載入索引為0的參數到託管堆棧里並存入指令流
ilGen.Emit(OpCodes.Ret);//如果存在返回值就將值推送到堆棧里並存入指令流
tb.CreateType();//創建該類型
ab.Save(am.Name+".dll");//保存到bin目錄

}

通過上面的操作後,形成IL中間語言,然後再使用反編譯工具,查看Dynamic.dll,可以看到它的原型如下:

//App是上面定義的類名,T是泛型類型參數,且約束於引用類型class,且存在公共的
//無參的構造函數,其它,如欄位,方法同上。
publicclassApp<T>whereT:class,new()
{
privateT_foo;
publicvoidBar()
{
this;
}
}

3. C#怎樣防止反編譯

我使用的方法是利用加殼工具:virboxProtectorStandalone。直接進行加殼。高級混淆、虛擬化代碼、智能壓縮加密策略。如果要授權控制,可使用許可版本的virboxProtector。

未經加殼保護的 ILspy 反編譯效果如下:

public int add(int a, int b){
return a + b;}public int div(int a, int b){
return a / b;}public int mul(int a, int b){
return a * b;}public int sub(int a, int b){
return a - b;}

解決方案:

深思自主研發了為 C# .net 語言做保護的外殼(Virbox Protector)。將C# .net 編譯成的執行程序(.exe),動態庫(.dll)直接拖入加殼工具即可完成保護操作,十分方便。並且在效果上已經完全看不到源碼中的邏輯。

加密後的效果

public int add(int a, int b){
return (int)dm.dynamic_method((object)this, System.Reflection.MethodBase.GetCurrentMethod(), 16416u, 21, 16384u, 32u, 31516u, 5).Invoke(this, new object[]
{
this,
a,
b
});}
public int div(int a, int b){
return (int)dm.dynamic_method((object)this, System.Reflection.MethodBase.GetCurrentMethod(), 16956u, 21, 16924u, 32u, 31516u, 2).Invoke(this, new object[]
{
this,
a,
b
});}
public int mul(int a, int b){
return (int)dm.dynamic_method((object)this, System.Reflection.MethodBase.GetCurrentMethod(), 16776u, 21, 16744u, 32u, 31516u, 3).Invoke(this, new object[]
{
this,
a,
b
});}
public int sub(int a, int b){
return (int)dm.dynamic_method((object)this, System.Reflection.MethodBase.GetCurrentMethod(), 16596u, 21, 16564u, 32u, 31516u, 4).Invoke(this, new object[]
{
this,
a,
b
});}

架構支持

IIS 服務架構的後台邏輯 DLL 文件

windows PC 應用程序 EXE 文件

windows PC 應用程序動態庫 DLL 文件

UG等第三方繪圖工具使用的 DLL 文件

Unity3d 編譯使用的 DLL 文件

4. 關於android軟體中的so文件!

*.so文件是linux平台下的動態鏈接庫,反編譯動態鏈接庫參見windows下*.dll文件的反編譯,類似的
Linux:是一套免費使用和自由傳播的類Unix操作系統,是一個基於POSIX和UNIX的多用戶、多任務、支持多線程和多CPU的操作系統。它能運行主要的UNIX工具軟體、應用程序和網路協議。它支持32位和64位硬體。Linux繼承了Unix以網路為核心的設計思想,是一個性能穩定的多用戶網路操作系統。
Linux操作系統誕生於1991 年10 月5 日(這是第一次正式向外公布時間)。Linux存在著許多不同的Linux版本,但它們都使用了Linux內核。Linux可安裝在各種計算機硬體設備中,比如手機、平板電腦、路由器、視頻游戲控制台、台式計算機、大型機和超級計算機。嚴格來講,Linux這個詞本身只表示Linux內核,但實際上人們已經習慣了用Linux來形容整個基於Linux內核,並且使用GNU 工程各種工具和資料庫的操作系統。

動態鏈接庫:英文為DLL,是Dynamic Link Library 的縮寫形式,DLL是一個包含可由多個程序同時使用的代碼和數據的庫,DLL不是可執行文件。動態鏈接提供了一種方法,使進程可以調用不屬於其可執行代碼的函數。函數的可執行代碼位於一個 DLL 中,該 DLL 包含一個或多個已被編譯、鏈接並與使用它們的進程分開存儲的函數。DLL 還有助於共享數據和資源。多個應用程序可同時訪問內存中單個DLL 副本的內容。DLL 是一個包含可由多個程序同時使用的代碼和數據的庫。

DLL文件又稱「應用程序拓展」,是軟體文件類型。在Windows中,許多應用程序並不是一個完整的可執行文件,它們被分割成一些相對獨立的動態鏈接庫,即DLL文件,放置於系統中。當我們執行某一個程序時,相應的DLL文件就會被調用。一個應用程序可使用多個DLL文件,一個DLL文件也可能被不同的應用程序使用,這樣的DLL文件被稱為共享DLL文件。[1]

5. Dynamic和Var的區別及dynamic使用詳解

1.var聲明一個局部變數只是一種簡化語法,它要求編譯器根據一個表達式推斷具體的數據類型。

2.var只能用於聲明方法內部的局部變數,而dynamic可用於局部變數,欄位,參數。

3.表達式不能轉型為var,但能轉型為dynamic。

4.必須顯式初始化用var聲明的變數,但無需初始化用dynam聲明的變數。

//var 在編譯階段已經確定類型
// var varError;
var isIntType = ;
isIntType.ToString();
//dynamic在編譯期間不進行任何
//的類型檢查,而是將類型檢查放到
//了運行期
dynamic dyn = ;
// dynamic dynOk;
dyn = "hello world";
//error 字元串沒有 [ fn不存在的方法 ] 的方法
//但在語法檢查時通過,不會提示語法錯誤
var s = dyn.fn不存在的方法();//runtime error

由於dynamic在運行時才檢查類型,所以有時候會出現錯誤,因此使用它必須得法,那麼何時使用它比較方便呢?我們先定義一個Person類,然後用反射進行動態調用起Talk方法:

class Person
{
public void Talk(string msg)
{
MessageBox.Show(msg);
}
}

//dynamic 在反射時候可以簡化代碼
System.Type t = typeof(Person);
var obj = Activator.CreateInstance(t, null);
t.InvokeMember("Talk", System.Reflection.BindingFlags.InvokeMethod, null, obj, new object[] { "hell world!" });

在反射的時候,傳統的方法的方法調用往往比較繁瑣,而用dyanmic則非常簡化,而且直觀:

dynamic obj = Activator.CreateInstance(t, null);
obj.Talk("hell world!");

因此,dynamic只要使用得法,還是一個C#一個非常好的特徵。

6. C#的dynamic使用中有什麼需要注意的地方,以免濫用

主要有兩點。

  1. dynamic成員在編譯時會跳過所有的靜態類型檢查,這意味著它永遠不會被報編譯錯誤,也就是說你得不到編譯器的幫助而只能在運行時發現錯誤。編碼時請做好檢查或者添加必要的日誌。此外,涉及dynamic成員的代碼並不會被編譯器優化。

  2. 我的測試數據表明在.NET 4.5下訪問dynamic對象的一個成員時需要的時間大約是訪問靜態類型的靜態成員的12倍,但是它依舊比反射訪問一個靜態類型的靜態成員要快約9倍。如果是為了避免復雜的業務邏輯和避開反射可以考慮一用。


我個人的標準是,優先強類型,多寫一個類或介面不算什麼事兒。不到萬不得已不寫dynamic,哪怕它可能節省了很多代碼。

7. 新人求解匯編

說明: rax, rsp ,rip之類的都是64位寄存器.
開始:
xx43c: 為局部變數開辟堆棧空間.
xx440: 取地址 _DYNAMIC+0x190入rax寄存器
xx447: test 最普通用法,非空測試
xx44a: rax == null 就跳轉到 xx44e:
xx44c: 調用rax中存入的函數地址. ; 溫馨提示, dynamic + xx這樣的風格估計是指向虛函數表中的某個函數.
xx44e: 釋放為局部變數申請的空間.
結束.
c語言翻譯一下:
void gmon_start()
{
void (*pfn)();
pfn= &DYNAMIC + 0x190;
(*pfn)();
}
不過看匯編代碼我估計可能會是某種面向對象的語言開發程序反編譯回來的.

8. 跪求AS 3.0 高手解答:語法錯誤: leftbrace 應在 leftparen 之前。

這個程序進入就會直接返回一個空值,有什麼意義呢?再一個類名的問題,你這個類名到底是什麼?是txt_(max_13character)還是txt_?而且作為類,構造函數中沒有()是錯誤的主要原因。正確的應該是
package
{
import flash.display.*;
dynamic public class txt_(max_13_character) extends MovieClip
{
public function txt_(max_13_character)()
{

}
}
}
在這里,我去掉了return,因為沒有任何可返回的東西,要是加上這句,最容易出錯。在這里只能寫一個空的構造函數了。主要是不知你要表達的是啥意思

9. 如何還原 c#linq 反編譯

1.框架搭建
1.1 將struts2中的jar文件導入到項目中
commons-fileupload-1.2.1.jar,commons-io-1.3.2.jar,freemarker-2.3.15.jar,ognl-2.7.3.jar
struts2-core-2.1.8.1.jar,xwork-core-2.1.6.jar
1.2 將struts.xml文件拷貝到項目的src目錄下
1.3 修改web.xml文件
添加:
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.action中方法的調用方式
2.1 自動方法調用(只能調用execute)
2.2 指定方法調用(通過設置action標簽中的method屬性)
2.3 動態方法調用(在調用時,在action後加!方法名稱,如:login!deletUser)
注意:<constant name="struts.enable.DynamicMethodInvocation" value="true" />
2.4 通配符調用
3. action接收客戶端參數的方式
3.1 直接在action中定義參數變數,並生成set和get方法
3.2 定義接收參數的類
注意:都要為action的成員變數提供get和set方法
3.3 讓action實現ModelDriven介面,並實現裡面的getModel方法
4.獲取request,session,application的方式
4.1 用ActionContext獲取,實際上獲取到的都是Map對象
4.2 用ServletActionContext獲取,獲取到的是基於Servlet API的對象
4.3 讓action實現RequestAware,SessionAware,ApplicationAware介面,並實現裡面的方法
5.四種轉向
5.1 action轉發到頁面(默認)
5.2 action重定向到頁面 <result type="redirect">
5.3 action轉發到action <result type="chain">
<param name="actionName">login</param>
<param name="nameSpace">/login</param>
<param name="method">login</param>
</result>
5.4 action重定向到action <result type="redirectAction">login</result>

10. CPU和CPUID是什麼關系

在 Linux 2.4 內核中,用戶態 Ring3 代碼請求內核態 Ring0 代碼完成某些功能是通過系統調用完成的,而系統調用的是通過軟中斷指令(int 0x80)實現的。在 x86 保護模式中,處理 INT 中斷指令時,CPU 首先從中斷描述表 IDT 取出對應的門描述符,判斷門描述符的種類,然後檢查門描述符的級別 DPL 和 INT 指令調用者的級別 CPL,當 CPL<=DPL 也就是說 INT 調用者級別高於描述符指定級別時,才能成功調用,最後再根據描述符的內容,進行壓棧、跳轉、許可權級別提升。內核代碼執行完畢之後,調用 IRET 指令返回,IRET 指令恢復用戶棧,並跳轉會低級別的代碼。

其實,在發生系統調用,由 Ring3 進入 Ring0 的這個過程浪費了不少的 CPU 周期,例如,系統調用必然需要由 Ring3 進入 Ring0(由內核調用 INT 指令的方式除外,這多半屬於 Hacker 的內核模塊所為),許可權提升之前和之後的級別是固定的,CPL 肯定是 3,而 INT 80 的 DPL 肯定也是 3,這樣 CPU 檢查門描述符的 DPL 和調用者的 CPL 就是完全沒必要。正是由於如此,Intel x86 CPU 從 PII 300(Family 6,Model 3,Stepping 3)之後,開始支持新的系統調用指令 sysenter/sysexit。sysenter 指令用於由 Ring3 進入 Ring0,SYSEXIT 指令用於由 Ring0 返回 Ring3。由於沒有特權級別檢查的處理,也沒有壓棧的操作,所以執行速度比 INT n/IRET 快了不少。

不同系統調用方式的性能比較:

下面是一些來自互聯網的有關 sysenter/sysexit 指令和 INT n/IRET 指令在 Intel Pentium CPU 上的性能對比:

表1:系統調用性能測試測試硬體:Intel® Pentium® III CPU, 450 MHzProcessor Family: 6 Model: 7 Stepping: 2

用戶模式花費的時間 核心模式花費的時間
基於 sysenter/sysexit 指令的系統調用 9.833 microseconds 6.833 microseconds
基於中斷 INT n 指令的系統調用 17.500 microseconds 7.000 microseconds

數據來源:[1]

數據來源:[2]

表2:各種 CPU 上 INT 0x80 和 SYSENTER 執行速度的比較

CPU Int0x80 sysenter
Athlon XP 1600+ 277 169
800MHz mode 1 athlon 279 170
2.8GHz p4 northwood ht 1152 442

上述數據為對 100000 次 getppid() 系統調用所花費的 CPU 時鍾周期取的平均值
數據來源[3]

自這種技術推出之後,人們一直在考慮在 Linux 中加入對這種指令的支持,在 Kernel.org 的郵件列表中,主題為 "Intel P6 vs P7 system call performance" 的大量郵件討論了採用這種指令的必要性,郵件中列舉的理由主要是 Intel 在 Pentium 4 的設計上存在問題,造成 Pentium 4 使用中斷方式執行的系統調用比 Pentium 3 以及 AMD Athlon 所耗費的 CPU 時鍾周期多上 5~10 倍。因此,在 Pentium 4 平台上,通過 sysenter/sysexit 指令來執行系統調用已經是刻不容緩的需求。

sysenter/sysexit 系統調用的機制:

在 Intel 的軟體開發者手冊第二、三卷(Vol.2B,Vol.3)中,4.8.7 節是關於 sysenter/sysexit 指令的詳細描述。手冊中說明,sysenter 指令可用於特權級 3 的用戶代碼調用特權級 0 的系統內核代碼,而 SYSEXIT 指令則用於特權級 0 的系統代碼返回用戶空間中。sysenter 指令可以在 3,2,1 這三個特權級別調用(Linux 中只用到了特權級 3),而 SYSEXIT 指令只能從特權級 0 調用。

執行 sysenter 指令的系統必須滿足兩個條件:1.目標 Ring 0 代碼段必須是平坦模式(Flat Mode)的 4GB 的可讀可執行的非一致代碼段。2.目標 RING0 堆棧段必須是平坦模式(Flat Mode)的 4GB 的可讀可寫向上擴展的棧段。

在 Intel 的手冊中,還提到了 sysenter/sysexit 和 int n/iret 指令的一個區別,那就是 sysenter/sysexit 指令並不成對,sysenter 指令並不會把 SYSEXIT 所需的返回地址壓棧,sysexit 返回的地址並不一定是 sysenter 指令的下一個指令地址。調用 sysenter/sysexit 指令地址的跳轉是通過設置一組特殊寄存器實現的。這些寄存器包括:

SYSENTER_CS_MSR - 用於指定要執行的 Ring 0 代碼的代碼段選擇符,由它還能得出目標 Ring 0 所用堆棧段的段選擇符;

SYSENTER_EIP_MSR - 用於指定要執行的 Ring 0 代碼的起始地址;

SYSENTER_ESP_MSR-用於指定要執行的Ring 0代碼所使用的棧指針

這些寄存器可以通過 wrmsr 指令來設置,執行 wrmsr 指令時,通過寄存器 edx、eax 指定設置的值,edx 指定值的高 32 位,eax 指定值的低 32 位,在設置上述寄存器時,edx 都是 0,通過寄存器 ecx 指定填充的 MSR 寄存器,sysenter_CS_MSR、sysenter_ESP_MSR、sysenter_EIP_MSR 寄存器分別對應 0x174、0x175、0x176,需要注意的是,wrmsr 指令只能在 Ring 0 執行。

這里還要介紹一個特性,就是 Ring0、Ring3 的代碼段描述符和堆棧段描述符在全局描述符表 GDT 中是順序排列的,這樣只需知道 SYSENTER_CS_MSR 中指定的 Ring0 的代碼段描述符,就可以推算出 Ring0 的堆棧段描述符以及 Ring3 的代碼段描述符和堆棧段描述符。

在 Ring3 的代碼調用了 sysenter 指令之後,CPU 會做出如下的操作:

1. 將 SYSENTER_CS_MSR 的值裝載到 cs 寄存器

2. 將 SYSENTER_EIP_MSR 的值裝載到 eip 寄存器

3. 將 SYSENTER_CS_MSR 的值加 8(Ring0 的堆棧段描述符)裝載到 ss 寄存器。

4. 將 SYSENTER_ESP_MSR 的值裝載到 esp 寄存器

5. 將特權級切換到 Ring0

6. 如果 EFLAGS 寄存器的 VM 標志被置位,則清除該標志

7. 開始執行指定的 Ring0 代碼

在 Ring0 代碼執行完畢,調用 SYSEXIT 指令退回 Ring3 時,CPU 會做出如下操作:

1. 將 SYSENTER_CS_MSR 的值加 16(Ring3 的代碼段描述符)裝載到 cs 寄存器

2. 將寄存器 edx 的值裝載到 eip 寄存器

3. 將 SYSENTER_CS_MSR 的值加 24(Ring3 的堆棧段描述符)裝載到 ss 寄存器

4. 將寄存器 ecx 的值裝載到 esp 寄存器

5. 將特權級切換到 Ring3

6. 繼續執行 Ring3 的代碼

由此可知,在調用 SYSENTER 進入 Ring0 之前,一定需要通過 wrmsr 指令設置好 Ring0 代碼的相關信息,在調用 SYSEXIT 之前,還要保證寄存器edx、ecx 的正確性。

如何得知 CPU 是否支持 sysenter/sysexit 指令

根據 Intel 的 CPU 手冊,我們可以通過 CPUID 指令來查看 CPU 是否支持 sysenter/sysexit 指令,做法是將 EAX 寄存器賦值 1,調用 CPUID 指令,寄存器 edx 中第 11 位(這一位名稱為 SEP)就表示是否支持。在調用 CPUID 指令之後,還需要查看 CPU 的 Family、Model、Stepping 屬性來確認,因為據稱 Pentium Pro 處理器會報告 SEP 但是卻不支持 sysenter/sysexit 指令。只有 Family 大於等於 6,Model 大於等於 3,Stepping 大於等於 3 的時候,才能確認 CPU 支持 sysenter/sysexit 指令。

Linux 對 sysenter/sysexit 系統調用方式的支持

在 2.4 內核中,直到最近的發布的 2.4.26-rc2 版本,沒有加入對 sysenter/sysexit 指令的支持。而對 sysenter/sysexit 指令的支持最早是2002 年,由 Linus Torvalds 編寫並首次加入 2.5 版內核中的,經過多方測試和多次 patch,最終正式加入到了 2.6 版本的內核中。

http://kerneltrap.org/node/view/531/1996

http://lwn.net/Articles/18414/

具體談到系統調用的完成,不能孤立的看內核的代碼,我們知道,系統調用多被封裝成庫函數提供給應用程序調用,應用程序調用庫函數後,由 glibc 庫負責進入內核調用系統調用函數。在 2.4 內核加上老版的 glibc 的情況下,庫函數所做的就是通過 int 指令來完成系統調用,而內核提供的系統調用介面很簡單,只要在 IDT 中提供 INT 0x80 的入口,庫就可以完成中斷調用。

在 2.6 內核中,內核代碼同時包含了對 int 0x80 中斷方式和 sysenter 指令方式調用的支持,因此內核會給用戶空間提供一段入口代碼,內核啟動時根據 CPU 類型,決定這段代碼採取哪種系統調用方式。對於 glibc 來說,無需考慮系統調用方式,直接調用這段入口代碼,即可完成系統調用。這樣做還可以盡量減少對 glibc 的改動,在 glibc 的源碼中,只需將 "int $0x80" 指令替換成 "call 入口地址" 即可。

下面,以 2.6.0 的內核代碼配合支持 SYSENTER 調用方式的 glibc2.3.3 為例,分析一下系統調用的具體實現。

內核在啟動時做的准備

前面說到的這段入口代碼,根據調用方式分為兩個文件,支持 sysenter 指令的代碼包含在文件 arch/i386/kernel/vsyscall-sysenter.S 中,支持int中斷的代碼包含在arch/i386/kernel/vsyscall-int80.S中,入口名都是 __kernel_vsyscall,這兩個文件編譯出的二進制代碼由arch/i386/kernel/vsyscall.S所包含,並導出起始地址和結束地址。

2.6 內核在啟動的時候,調用了新增的函數sysenter_setup(參見arch/i386/kernel/sysenter.c),在這個函數中,內核將虛擬內存空間的頂端一個固定地址頁面(從0xffffe000開始到0xffffeffff的4k大小)映射到一個空閑的物理內存頁面。然後通過之前執行CPUID的指令得到的數據,檢測CPU是否支持sysenter/sysexit指令。如果CPU不支持,那麼將採用INT調用方式的入口代碼拷貝到這個頁面中,然後返回。相反,如果CPU支持SYSETER/SYSEXIT指令,則將採用SYSENTER調用方式的入口代碼拷貝到這個頁面中。使用宏 on_each_cpu在每個CPU上執行enable_sep_cpu這個函數。

在enable_sep_cpu函數中,內核將當前CPU的TSS結構中的ss1設置為當前內核使用的代碼段,esp1設置為該TSS結構中保留的一個256位元組大小的堆棧。在X86中,TSS結構中ss1和esp1本來是用於保存Ring 1進程的堆棧段和堆棧指針的。由於內核在啟動時,並不能預知調用sysenter指令進入Ring 0後esp的確切值,而應用程序又無權調用wrmsr指令動態設置,所以此時就借用esp1指向一個固定的緩沖區來填充這個MSR寄存器,由於Ring 1根本沒被啟用,所以並不會對系統造成任何影響。在下面的文章中會介紹進入Ring 0之後,內核如何修復ESP來指向正確的Ring 0堆棧。關於TSS結構更細節的應用可參考代碼include/asm-i386/processor.h)。

然後,內核通過wrmsr(msr,val1,val2)宏調用wrmsr指令對當前CPU設置MSR寄存器,可以看出調用宏的第三個參數即edx都被設置為0。其中SYSENTER_CS_MSR的值被設置為當前內核用的所在代碼段;SYSENTER_ESP_MSR被設置為esp1,即指向當前CPU的 TSS結構中的堆棧;SYSENTER_EIP_MSR則被設置為內核中處理sysenter指令的介面函數sysenter_entry(參見 arch/i386/kernel/entry.S)。這樣,sysenter指令的准備工作就完成了。

通過內核在啟動時進行這樣的設置,在每個進程的進程空間中,都能訪問到內核所映射的這個代碼頁面,當然這個頁面對於應用程序來說是只讀的。我們通過新版的ldd工具查看任意一個可執行程序,可以看到下面的結果:

[root@test]# file dynamic
dynamic: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped
[root@test]# ldd dynamic
linux-gate.so.1 => (0xffffe000)
libc.so.6 => /lib/tls/libc.so.6 (0x4002c000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

這個所謂的"linux-gate.so.1"的內容就是內核映射的代碼,系統中其實並不存在這樣一個鏈接庫文件,它的名字是由ldd自己起的,而在老版本的ldd中,雖然能夠檢測到這段代碼,但是由於沒有命名而且在系統中找不到對應鏈接庫文件,所以會有一些顯示上的問題。有關這個問題的背景,可以參考下面這個網址: http://sources.redhat.com/ml/libc-alpha/2003-09/msg00263.html。

由用戶態經庫函數進入內核態

為了配合內核使用新的系統調用方式,glibc中要做一定的修改。新的glibc-2.3.2(及其以後版本中)中已經包含了這個改動,在glibc源代碼的sysdeps/unix/sysv/linux/i386/sysdep.h文件中,處理系統調用的宏INTERNAL_SYSCALL在不同的編譯選項下有不同的結果。在打開支持sysenter/sysexit指令的選項I386_USE_SYSENTER下,系統調用會有兩種方式,在靜態鏈接(編譯時加上-static選項)情況下,採用"call *_dl_sysinfo"指令;在動態鏈接情況下,採用"call *%gs:0x10"指令。這兩種情況由glibc庫採用哪種方法鏈接,實際上最終都相當於調用某個固定地址的代碼。下面我們通過一個小小的程序,配合 gdb來驗證。

首先是一個靜態編譯的程序,代碼很簡單:

main()
{
getuid();
}

將代碼加上static選項用gcc靜態編譯,然後用gdb裝載並反編譯main函數。

[root@test opt]# gcc test.c -o ./static -static
[root@test opt]# gdb ./static
(gdb) disassemble main
0x08048204 <main+0>: push %ebp
0x08048205 <main+1>: mov %esp,%ebp
0x08048207 <main+3>: sub $0x8,%esp
0x0804820a <main+6>: and $0xfffffff0,%esp
0x0804820d <main+9>: mov $0x0,%eax
0x08048212 <main+14>: sub %eax,%esp
0x08048214 <main+16>: call 0x804cb20 <__getuid>
0x08048219 <main+21>: leave
0x0804821a <main+22>: ret

可以看出,main函數中調用了__getuid函數,接著反編譯__getuid函數。

(gdb) disassemble 0x804cb20
0x0804cb20 <__getuid+0>: push %ebp
0x0804cb21 <__getuid+1>: mov 0x80aa028,%eax
0x0804cb26 <__getuid+6>: mov %esp,%ebp
0x0804cb28 <__getuid+8>: test %eax,%eax
0x0804cb2a <__getuid+10>: jle 0x804cb40 <__getuid+32>
0x0804cb2c <__getuid+12>: mov $0x18,%eax
0x0804cb31 <__getuid+17>: call *0x80aa054
0x0804cb37 <__getuid+23>: pop %ebp
0x0804cb38 <__getuid+24>: ret

上面只是__getuid函數的一部分。可以看到__getuid將eax寄存器賦值為getuid系統調用的功能號0x18然後調用了另一個函數,這個函數的入口在哪裡呢?接著查看位於地址0x80aa054的值。

(gdb) X 0x80aa054
0x80aa054 <_dl_sysinfo>: 0x0804d7f6

看起來不像是指向內核映射頁面內的代碼,但是,可以確認,__dl_sysinfo指針的指向的地址就是0x80aa054。下面,我們試著啟動這個程序,然後停在程序第一條語句,再查看這個地方的值。

(gdb) b main
Breakpoint 1 at 0x804820a
(gdb) r
Starting program: /opt/static
Breakpoint 1, 0x0804820a in main ()
(gdb) X 0x80aa054
0x80aa054 <_dl_sysinfo>: 0xffffe400

可以看到,_dl_sysinfo指針指向的數值已經發生了變化,指向了0xffffe400,如果我們繼續運行程序,__getuid函數將會調用地址0xffffe400處的代碼。

接下來,我們將上面的代碼編譯成動態鏈接的方式,即默認方式,用gdb裝載並反編譯main函數

[root@test opt]# gcc test.c -o ./dynamic
[root@test opt]# gdb ./dynamic
(gdb) disassemble main
0x08048204 <main+0>: push %ebp
0x08048205 <main+1>: mov %esp,%ebp
0x08048207 <main+3>: sub $0x8,%esp
0x0804820a <main+6>: and $0xfffffff0,%esp
0x0804820d <main+9>: mov $0x0,%eax
0x08048212 <main+14>: sub %eax,%esp
0x08048214 <main+16>: call 0x8048288
0x08048219 <main+21>: leave
0x0804821a <main+22>: ret

由於libc庫是在程序初始化時才被裝載,所以我們先啟動程序,並停在main第一條語句,然後反匯編getuid庫函數



(gdb) b main
Breakpoint 1 at 0x804820a
(gdb) r
Starting program: /opt/dynamic
Breakpoint 1, 0x0804820a in main ()
(gdb) disassemble getuid
Dump of assembler code for function getuid:
0x40219e50 <__getuid+0>: push %ebp
0x40219e51 <__getuid+1>: mov %esp,%ebp
0x40219e53 <__getuid+3>: push %ebx
0x40219e54 <__getuid+4>: call 0x40219e59 <__getuid+9>
0x40219e59 <__getuid+9>: pop %ebx
0x40219e5a <__getuid+10>: add $0x84b0f,%ebx
0x40219e60 <__getuid+16>: mov 0xffffd87c(%ebx),%eax
0x40219e66 <__getuid+22>: test %eax,%eax
0x40219e68 <__getuid+24>: jle 0x40219e80 <__getuid+48>
0x40219e6a <__getuid+26>: mov $0x18,%eax
0x40219e6f <__getuid+31>: call *%gs:0x10
0x40219e76 <__getuid+38>: pop %ebx
0x40219e77 <__getuid+39>: pop %ebp
0x40219e78 <__getuid+40>: ret

可以看出,庫函數getuid將eax寄存器設置為getuid系統調用的調用號0x18,然後調用%gs:0x10所指向的函數。在gdb中,無法查看非DS段的數據內容,所以無法查看%gs:0x10所保存的實際數值,不過我們可以通過編程的辦法,內嵌匯編將%gs:0x10的值賦予某個局部變數來得到這個數值,而這個數值也是0xffffe400,具體代碼這里就不再贅述。

由此可見,無論是靜態還是動態方式,最終我們都來到了0xffffe400這里的一段代碼,這里就是內核為我們映射的系統調用入口代碼。在gdb中,我們可以直接反匯編來查看這里的代碼

(gdb) disassemble 0xffffe400 0xffffe414
Dump of assembler code from 0xffffe400 to 0xffffe414:0xffffe400: push %ecx
0xffffe401: push %edx
0xffffe402: push %ebp
0xffffe403: mov %esp,%ebp
0xffffe405: sysenter
0xffffe407: nop
0xffffe408: nop
0xffffe409: nop
0xffffe40a: nop
0xffffe40b: nop
0xffffe40c: nop
0xffffe40d: nop
0xffffe40e: jmp 0xffffe403
0xffffe410: pop %ebp
0xffffe411: pop %edx
0xffffe412: pop %ecx
0xffffe413: ret
End of assembler mp.

這段代碼正是arch/i386/kernel/vsyscall- sysenter.S文件中的代碼。其中,在sysenter之前的是入口代碼,在0xffffe410開始的是內核返回處理代碼(後面提到的 SYSENTER_RETURN即指向這里)。在入口代碼中,首先是保存當前的ecx,edx(由於sysexit指令需要使用這兩個寄存器)以及 ebp。然後調用sysenter指令,跳轉到內核Ring 0代碼,也就是sysenter_entry入口處。

內核中的處理和返回

sysenter_entry整個的實現可以參見arch/i386/kernel/entry.S。內核處理SYSENTER的代碼和處理INT的代碼不太一樣。通過sysenter指令進入Ring 0之後,由於當前的ESP並非指向正確的內核棧,而是當前CPU的TSS結構中的一個緩沖區(參見上文),所以首先要解決的是修復ESP,幸運的是,TSS結構中ESP0成員本身就保存有Ring 0狀態的ESP值,所以在這里將TSS結構中ESP0的值賦予ESP寄存器。將ESP恢復成指向正確的堆棧之後,由於SYSENTER不是通過調用門進入Ring 0,所以在堆棧中的上下文和使用INT指令的不一樣,INT指令進入Ring 0後棧中會保存如下的值。

低地址

返回用戶態的EIP
用戶態的CS
用戶態的EFLAGS
用戶態的ESP
用戶態的SS(和DS相同)
高地址
因此,為了簡化和重用代碼,內核會用pushl指令往棧中放入上述各值,值得注意的是,內核在棧中放入的相對應用戶態EIP的值,是一個代碼標簽 SYSENTER_RETURN,在vsyscall-sysenter.S可以看到,它就在sysenter指令的後面(在它們之間,有一段NOP,是內核返回出錯時的處理代碼)。接下來,處理系統調用的代碼就和中斷方式的處理代碼一模一樣了,內核保存所有的寄存器,然後系統調用表找到對應系統調用的入口,完成調用。最後,內核從棧中取出前面存入的用戶態的EIP和ESP,存入edx和ecx寄存器,調用SYSEXIT指令返回用戶態。返回用戶態之後,從棧中取出ESP,edx,ecx,最終返回glibc庫。

其它操作系統以及其它硬體平台的支持

值得一提的是,從 Windows XP 開始,Windows 的系統調用方式也從軟中斷 int 0x2e 轉換到採用 sysenter 方式,由於完全不再支持 int 方式,因此 Windows XP 的對 CPU 的最低配置要求是 PentiumII 300MHz。在其它的操作系統例如 *BSD 系列,目前並沒有提供對 sysenter 指令的支持。

在 CPU 方面,AMD 的 CPU 支持一套與之對應的指令 SYSCALL/SYSRET。在純 32 位的 AMD CPU 上,還沒有支持 sysenter 指令,而在 AMD 推出的 AMD64 系列 CPU 上,處於某些模式的情況下,CPU 能夠支持 sysenter/sysexit 指令。在 Linux 內核針對 AMD64 架構的代碼中,採用的還是 SYSCALL/SYSRET 指令。至於這兩種指令最終誰將成為標准,目前還無法得出結論。

未來

我們將 Intel 的 sysenter/sysexit 指令,AMD 的 SYSCALL/SYSRET 指令統稱為"快速系統調用指令"。"快速系統調用指令"比起中斷指令來說,其消耗時間必然會少一些,但是隨著 CPU 設計的發展,將來應該不會再出現類似 Intel Pentium4 這樣懸殊的差距。而"快速系統調用指令"比起中斷方式的系統調用方式,還存在一定局限,例如無法在一個系統調用處理過程中再通過"快速系統調用指令"調用別的系統調用。因此,並不一定每個系統調用都需要通過"快速系統調用指令"來實現。比如,對於復雜的系統調用例如 fork,兩種系統調用方式的時間差和系統調用本身運行消耗的時間來比,可以忽略不計,此處採取"快速系統調用指令"方式沒有什麼必要。而真正應該使用" 快速系統調用指令"方式的,是那些本身運行時間很短,對時間精確性要求高的系統調用,例如 getuid、gettimeofday 等等。因此,採取靈活的手段,針對不同的系統調用採取不同的方式,才能得到最優化的性能和實現最完美的功能。

參考資料

[1] VxWorks Optimized for Intel Architecture, Hdei Nunoe, Wind River, Member of Technical Staff Leo Samson, Wind River, Technical Marketing Engineer David Hillyard, Intel Corporation, Mgr., Platform Architect

[2] Kernel Entry / Kernel Exit , Marcus Voelp & University Karlsruhe

[3] Dave Jones' blog, http://diary.codemonkey.org.uk/index.php?month=12&year=2002

[4] Linux 內核源碼 v2.6.0 http://www.kernel.org/ [Linus Torvalds,2004]

[5] GNU C Library glibc 2.3.3 源碼 http://www.gnu.org/software/libc/libc.html

Linux Kernel Mailing List 中對系統調用方式的討論: [5] Linux Kernel Mailing List, "Intel P6 vs P7 system call performance" http://www.ussg.iu.e/hypermail/linux/kernel/0212.1/index.html#1286 http://www.ussg.iu.e/hypermail/linux/kernel/0212.3/index.html#54

Linux 內核首次引入對 sysenter/sysexit 指令的支持: [6] Linux Kernel Mailing List, "Add "sysenter" support on x86, and a "vsyscall" page." http://lwn.net/Articles/18414/

熱點內容
connectorpython 發布:2025-05-14 09:48:50 瀏覽:762
配置不好怎麼辦 發布:2025-05-14 09:46:40 瀏覽:622
數據流程圖中的數據存儲是指 發布:2025-05-14 09:46:39 瀏覽:445
我的世界伺服器id前綴mod 發布:2025-05-14 09:45:53 瀏覽:830
完整後台網站源碼 發布:2025-05-14 09:45:46 瀏覽:455
傳奇祝福腳本 發布:2025-05-14 09:34:12 瀏覽:571
電腦文件加密的軟體 發布:2025-05-14 09:29:20 瀏覽:354
擴展資料庫表空間 發布:2025-05-14 09:29:10 瀏覽:642
mongo存儲過程 發布:2025-05-14 09:27:54 瀏覽:715
伺服器的公網ip在哪看 發布:2025-05-14 09:18:30 瀏覽:253