對於NS3原始碼分析的反思與總結
-
構建斷點除錯環境是進行原始碼分析的第一步. 以下是VSCode配置檔案,以及開啟除錯的程式碼:
{
"version": "0.2.0",
"configurations": [
{
"name": "dctcp",
"type": "cppdbg",
"request": "launch",
"program": "/home/*****/ns3.35/ns-allinone-3.35/ns-3.35/build/scratch/${fileBasenameNoExtension}",
"args": [],
"stopAtEntry": false,
"cwd": "/home/*****/ns3.35/ns-allinone-3.35/ns-3.35/scratch",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
// "preLaunchTask": "build",
"miDebuggerPath": "/usr/bin/gdb",
"miDebuggerServerAddress": "localhost:1234"
},
]
}# 進行waf的配置: 關閉python介面,減少編譯量
./waf --disable-python configure
# 進行編譯:
./waf
# 將NS3 LOG輸出為檔案:
./waf --run scratch/test.cc > ns3_log.out 2>&1
# 開啟gdbserver進行斷點除錯:
./waf --run scratch/test.cc --command-template='gdbserver :1234 %s' # 這裡的 command-template 的 %s 相當於編譯後可執行檔案的全路徑 -
進行原始碼閱讀的時候我認為需要按照以下步驟進行分析:
-
瞭解應用系統基本的抽象概念:
例如: NS3中重要的抽象概念有: Node, NetDevice, Protocol, Channel, Packet, Socket, Scheduler 等
不需要了解每個抽象概念是怎麼實現的, 只需要對其的作用有一個大概的認識, 方便在後續環節中更好地理解模組之間地呼叫
-
對於應用的主迴圈或主方法有一個大致認識:
當然,這一點不知道對於其他應用是否成立. 在NS3中,
Simulator::Run ();
就是其主入口從主入口開始斷點, 通過進入主入口的函式找到執行模組的邏輯
-
對於核心容器中的內容進行分析:
由於NS3和很多其他面向物件的程式一樣使用了
interface
CallBack
進行物件之間的呼叫, 直接閱讀靜態的程式碼只能讀到介面的宣告,而不能獲得具體的物件資訊.這種情況下,可以通過在基類中建立自己的函式, 進行物件內容的列印. 為什麼不用斷點進行檢視? 因為在模擬程式中需要檢視的物件數量較大, 一般有十幾到幾十個, 如果使用斷點則需要清晰記住每個中斷的時候對應的是哪個物件, 非常的困難.
這是我在
Object
類中建立的進行物件內容列印的函式. 似乎是由於NS3中的物件的引用間存在迴圈, 導致如果放開遞迴限制會造成棧溢位. 所以我在這對遞迴的層數進行了限制.void Object::recurentPrintHelper(Ptr<Object> instance, size_t level){
std::string shifting = "";
for (size_t i = 0; i < level; i++)
{
shifting += "\t";
}
if (level>=1)
{
return;
}
if(instance->m_aggregates == NULL){
std::cout<<shifting<<"m_aggregates is NULL"<<std::endl;
return;
}
size_t N = instance->m_aggregates->n;
std::cout<<shifting<<"m_aggregates has "<<N<<" elements:"<<std::endl;
for (size_t i = 0; i < N; i++)
{
std::string instantName = instance->m_aggregates->buffer[i]->GetInstanceTypeId().GetName();
Ptr<Object> lowerLevelInstance = m_aggregates->buffer[i]->GetObject<Object>();
if (lowerLevelInstance!=0 && lowerLevelInstance->IsInitialized())
{
std::cout<<shifting<<i<<" "<<instantName<<std::endl;
recurentPrintHelper(lowerLevelInstance, level+1);
} else {
std::cout<<shifting<<i<<" "<<instantName<<"is empty or not inited"<<std::endl;
}
}
}
/*
m_aggregates has 18 elements:
0 ns3::Ipv4L3Protocol
1 ns3::Ipv6L3Protocol
2 ns3::Node
3 ns3::GlobalRouter
4 ns3::TrafficControlLayer
5 ns3::ArpL3Protocol
6 ns3::TcpSocketFactory
7 ns3::Icmpv4L4Protocol
8 ns3::Ipv4RawSocketFactory
9 ns3::Ipv6RawSocketFactory
10 ns3::Icmpv6L4Protocol
11 ns3::Ipv6ExtensionRoutingDemux
12 ns3::Ipv6ExtensionDemux
13 ns3::Ipv6OptionDemux
14 ns3::UdpL4Protocol
15 ns3::UdpSocketFactory
16 ns3::TcpL4Protocol
17 ns3::PacketSocketFactory
*/ -
對於重要的工作流程進行斷點:
這裡進行斷點分析有兩種方法:
-
在可能是關鍵點的函式的入口進行斷點, 當gdb到達斷點後儲存呼叫棧. 根據呼叫棧的順序依次進行程式碼的閱讀和分析
-
優點: 對於腦力消耗較少, 只要找準了關鍵函式, 其呼叫過程便清晰無比
-
缺點: 需要對程式碼有較好的總體認識, 如果沒有總體認識而只是進行瞎猜, 其耗費的時間不如靜下心來一行行的讀
-
建議:
-
當對程式碼總體有了一個比較清晰的瞭解之後再進行該種分析, 可以在保證效率的同時,兼顧準確率
-
該方法對於程式碼的分析不是100%可靠, 因為很多沒有被執行到的分支,或者是已經執行過的分支是無法在呼叫棧中體現的. 如果需要更加細緻的分析,還是使用第二種方法較好
-
-
-
在已知的主迴圈或重要函式入口進行斷點, 通過
step into
step over
等按鍵一邊進行程式碼的閱讀分析, 一邊進行函式的斷點更新.-
優點: 對於程式碼呼叫的各種細節可以進行了解, 對於一些沒有被執行到的程式碼分支可以主動的進行分析, 對於程式碼的瞭解更加全面
-
缺點: 需要主動記錄筆記, 當同時有多個需要分析的分支的時候,對腦力的消耗很大. 有時由於需要注意的細節過多導致最後忘記一開始是打算幹什麼
-
建議:
-
一般該方法用在需要對程式碼總體架構有一個基本瞭解的時候, 或者是需要對某個模組進行細緻瞭解的時候
-
勤記筆記, 記憶力和自控力非常寶貴, 不要將其浪費在對於呼叫順序的記憶上
-
-
-
-
最最最重要的一點: 充分利用程式碼文件和網路資源.
對於原始碼的學習我認為不能基於網路部落格, 但是基於官方文件是非常重要的. 通過搜尋引擎搜尋官方文件同時搜尋某些關鍵字可以有意想不到的收穫. 另外,如果進行程式碼閱讀的時候碰上了問題, 一個很重要的解決方案就是檢視官方文件對其的解釋.
在官方文件中獲取的一些資訊可以極高地提升程式碼除錯的效率:
例如:
網頁搜尋
NS3 structure
可以找到以下文件: https://www.nsnam.org/docs/architecture.pdf該文件中有這樣的內容: 直接明確了Node物件中的結構以及呼叫方式
此外,還有這個內容: 直接明確了模組的
Send()
函式是模組進行通訊的主要入口之一, 為後面的斷點分析省去了很多前置工作.所以在進行原始碼學習的時候, 不脫離官方文件是多麼的重要!!!
-
最後, 由淺入深才是學習的一般規律. 不要好高騖遠, 先將官方示例的實現細節, 執行邏輯搞清楚後再進行較為複雜的研究. 基於官方的
Tutorial
, 以first.cc
為研究物件分析其工作流程才能叫充分利用率官方的資源.
-
-
關於
NS3
中一些值得學習的實現細節的歸納:-
對於 C++ 回撥型別的實現: 基於泛型進行回撥以及回撥的引數的設定 (說實話我其實對這個回撥類的設計還沒太搞懂, 還需要再進行研究)
ns3::MemPtrCallbackImpl<
ns3::Ptr<ns3::Ipv4>,
void (ns3::Ipv4::*)(
ns3::Ptr<ns3::Packet>,
ns3::Ipv4Address,
ns3::Ipv4Address,
unsigned char,
ns3::Ptr<ns3::Ipv4Route>),
void, ns3::Ptr<ns3::Packet>,
ns3::Ipv4Address,
ns3::Ipv4Address,
unsigned char,
ns3::Ptr<ns3::Ipv4Route>,
ns3::empty,
ns3::empty,
ns3::empty,
ns3::empty
>::operator()
ns3::Callback<
void,
ns3::Ptr<ns3::Packet>,
ns3::Ipv4Address,
ns3::Ipv4Address,
unsigned char,
ns3::Ptr<ns3::Ipv4Route>,
ns3::empty,
ns3::empty,
ns3::empty,
ns3::empty
>::operator() -
基於離散事件的模擬
// 所有的事件都由以下介面順序進行呼叫:
ns3::DefaultSimulatorImpl::ProcessOneEvent;
--> ns3::EventImpl::Invoke;
--> ns3::MakeEvent<...>(...);
--> //具體的執行函式
// 在事件中通過鏈式的呼叫決定物件執行的順序:
while (!m_events->IsEmpty () && !m_stop)
{
ProcessOneEvent ();
}
// 對於需要持續一段時間的事件通過建立新的定時任務進行模擬:
PointToPointRemoteChannel::TransmitStart(){
//......
Time rxTime = Simulator::Now () + txTime + GetDelay ();
MpiInterface::SendPacket (p->Copy (), rxTime, dst->GetNode ()->GetId (), dst->GetIfIndex ());
}
// 即是通過定時任務開啟資料的傳輸, 並計算資料傳輸任務結束的事件, 然後建立一個任務結束的定時任務. 任務開始到任務結束的過程則不需要進行模擬. -
對於程式碼中基於
aggregate
的(COM)設計模式:-
將物件聚合到
Object
(主要是Node
)物件中, 保證了模組物件和網路節點的緊密繫結的同時減小了對於模組之間通訊與連線的約束. 適用於設計目的較為複雜, 需要有較高靈活度的程式. -
由於
使用 send 或回撥 介面進行模組之間通訊
是沒有程式碼強制執行的, 所以模組間的連線其實比較雜亂, 實際執行中的呼叫順序需要通過呼叫棧進行動態的檢視.
-
-
先寫這麼多吧, 如果後面發現還有什麼值得總結的會寫在另開的隨筆中