-
構建斷點調試環境是進行源碼分析的第一步. 以下是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 或回調 接口進行模塊之間通信
是沒有代碼強制執行的, 所以模塊間的連接其實比較雜亂, 實際運行中的調用順序需要通過調用棧進行動態的查看.
-
-
先寫這么多吧, 如果后面發現還有什么值得總結的會寫在另開的隨筆中