構建調試Linux內核(32位)網絡代碼的環境MenuOS系統
最后的目錄:
說明:qemu qemu-system-i386 qemu-system-x86_64
qemu-system-i386是32位的QEMU的命令
qemu-system-x86_64是64位的QEMU的命令
qemu 是軟鏈接到qemu-system-i386,二者是一樣的,如果qemu沒有軟鏈接,是無法執行的。
以下過程是回憶所寫,有些小細節可能記錯了,部分命令是手敲的,不一定對,僅供參考。
安裝,編譯linux內核
步驟 1:下載,配置編譯為32位
#如果想編譯為64位,請直接忽略此步驟最后一條命令,接步驟二開始,但是后面需要更改一些qemu命令的格式,要都按照64位來做,后面我大概提一下,但是具體細節我沒做,所以有什么坑我也不知道。
mkdir LinuxKernel #創建一個項目目錄
cd LinuxKernel
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar.xz #下載linux-5.0.1的內核,當然也可以下載其他版本的,就是有點慢。
xz -d linux-5.0.1.tar.xz #解壓
tar -xvf linux-5.0.1.tar
cd linux-5.0.1
sudo apt install build-essential flex bison libssl-dev libelf-dev libncurses-dev #安裝內核編譯所需的庫
make i386_defconfig #生成32位x86的配置文件
步驟 2:配置編譯需要debug信息
#針對32位的,步驟二可以在這步做,也可以在后面gdb調試那一步之前做,我當時是在那一步之前做的,但是現在想想沒必要編譯兩次,因為編譯真的需要很久,64位就在這兒做吧。
make menuconfig
#執行make menuconfig之后,會跳出一個圖形化界面,就在圖形化界面中完成以下操作,如果沒有跳出,或者報錯,自行解決界面大小適應問題:安裝vmware tool,或者在設置中調整分辨率。
1:選擇 Kernel hacking
2:選擇 Compile-time checks and compiler options
3:選擇 [ ]Compile the kernel with debug info
4:按Y 前面就多了一個 [*] Compile the kernel with debug info
5:選擇 save
6:按 esc,直到退出圖形化界面
步驟 3:編譯
make
漫長的等待開始了,直到編譯完成。
步驟 4:升級內核
#可以忽略此步驟!!!!因為這個步驟是老師上課講的的,但是我做的時候,機子在reboot的時候總是錯,所以后面就跳過了。
#歡迎大佬指出問題
uname -a
sudo make modules_install # ⚠️安裝前通過系統快照備份系統,以防出現故障前功盡棄
sudo make install
sudo update-grub
reboot
uname -a
制作根文件系統
步驟 1:QEMU虛擬機加載內核
cd ~/LinuxKernel/
sudo apt install qemu # 安裝qemu命令
qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage #qemu虛擬機加載 linux-5.0.1內核,這條命令可以不用執行,因為后面構造menuOS的makefile中是包含了這條命令的
步驟 2: 構造MenuOS
#下載menu系統,並在LinuxKernel目錄下建一個子目錄rootfs,當作menuOS的根目錄
git clone https://github.com/mengning/menu.git
mkdir rootfs
步驟2.1: 安裝libc6-dev-i386和修改Makefile
安裝libc6-dev-i386
sudo apt-get install libc6-dev-i386
修改makefile,我的做法是方式二
方式一
cd menu
vim Makefile
qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img #修改前
qemu-system-i386 -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img #修改后
wq
64位的就修改為 qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -initrd ../rootfs.img
方式二
#如果不想使用qemu-system-i386,仍然想使用qemu命令,就改為如下,然后執行一個軟鏈接
cd menu
vim Makefile
qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img #修改前
qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img #修改后
wq
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
步驟2.2 初始化根目錄
linux啟動后期會在根⽬錄中尋找⼀個應⽤程序來運⾏,在根⽬錄下提供init是⼀種可選⽅案
#在menu目錄下執行一下命令
make rootfs
結果應該是這樣
回車,然后執行help命令查看當前構建的menuOS系統中的命令 ,其他命令都可以,但是quit命令無效,hh。
gdb 調試
在執行gdb 調試之前,保證make menuconfig那個步驟已經執行,不然編譯的內核系統不含調試信息。
步驟 1:啟動gdb server
1 關閉 之前打開的menuOS系統界面
2 執行 qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s -S
為什么和老師的不一樣?em 我也不知道為什么,可能teacher給的命令只適合teache的機子,反正我又是一堆錯,這兒寫的命令也可能不適合你的機子。
所以多提供兩條參考命令,反正我的機子是不行的,說不定你的機子行呢,如下
qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S #(我的機子執行之后調試停不下來)
(32)qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -hda rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S
(64)qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -hda rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S
#(調試的時候,中途報錯:VFS,unable to mount rootfs on unkwon-block(x,x))
步驟2 gdb客戶端連接gdb server
#打開另一個終端
gdb
file ~/LinuxKernel/linux-5.0.1/vmlinux
break start_kernel
target remote:1234
c
list
如圖
多打幾個斷點看看,內核啟動的過程,具體細節再研究研究,看我后續部分
構建MenuOS的網絡功能
參考老師的實驗樓:https://www.shiyanlou.com/courses/1198
步驟1: 將 TCP 網絡通信程序的服務端集成到 MenuOS 系統中
cd ~/LinuxKernel
git clone https://github.com/mengning/linuxnet.git
cd linuxnet/lab2
make
cd ../../menu/
make rootfs #改一下Makefile
步驟2: 將 TCP 網絡通信程序的客戶端集成到 MenuOS 系統中
cd ~/LinuxKernel
git clone https://github.com/mengning/linuxnet.git
cd linuxnet/lab3
make rootfs #報錯之后,修改Makefile
結果如圖:menuOS下面已經多了replyhi,和 hello命令,后面再看細節。
后續。。。
linux 內核的啟動過程
start_kernel 部分代碼
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
set_task_stack_end_magic(&init_task); #設置0號進程的棧邊界。可以通過 gdb 調試查看 p init_task->pid 看到它的進程號
smp_setup_processor_id();
debug_objects_early_init();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_disabled = true;
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them. #設置中斷表
*/
boot_cpu_init();
page_address_init(); #初始化虛擬頁地址
pr_notice("%s", linux_banner);
setup_arch(&command_line);
/*
* Set up the the initial canary and entropy after arch
* and after adding latent and command line entropy.
*/
add_latent_entropy();
add_device_randomness(command_line, strlen(command_line));
boot_init_stack_canary();
mm_init_cpumask(&init_mm);
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas();
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
boot_cpu_hotplug_init();
start_kernel 代碼分析:參考博客:https://www.cnblogs.com/yjf512/p/5999532.html
說明:init_task 它是誰?他是0號進程,本質是一個結構體task_strcuk,即通用的進程描述符,它在哪創建?在cpu_startup_entry處創建,參考:https://www.cnblogs.com/dakewei/p/11558027.html
結構體task_strcuk是什么,參考:https://blog.csdn.net/qq_25424545/article/details/80289683
0號進程是如何調用rest_init創建1,2號進程 的,以及他們之間的關系?參考:https://www.cnblogs.com/alantu2018/p/8526970.html
如何往menu系統中添加tcp通訊功能的
以linuxnet/lab3為例
main.c
步驟 1 重啟網卡
BringUpNetInterface
為什么呢?為了配置網卡協議,以及綁定主機ip。
步驟2 構造命令和對應的handle函數
MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi);
MenuConfig中主要是設置一個結構體,將cmd和handle關聯起來
例如在menu中執行replyhi,就會調用相應的函數StartReplyhi,StartReplyhi中子進程負責調用replyhi,replyhi就是執行tcp服務器的代碼。
StartReplyhi
int StartReplyhi(int argc, char *argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr, "Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
Replyhi();
printf("Reply hi TCP Service Started!\n");
}
else
{
/* parent process */
printf("Please input hello...\n");
}
}
Replyhi
int Replyhi()
{
char szBuf[MAX_BUF_LEN] = "\0";
char szReplyMsg[MAX_BUF_LEN] = "hi\0";
InitializeService();
while (1)
{
ServiceStart();#宏定義 包括socket,bind,listen,accpet等函數
RecvMsg(szBuf);
SendMsg(szReplyMsg); #宏定義,就是調用recv函數
ServiceStop();
}
ShutdownService();#宏定義,就是調用close函數
return 0;
}
socket,bind,listen,accpet這些函數對應着以下系統調用
switch (call) {
case SYS_SOCKET:
err = __sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = __sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
case SYS_GETSOCKNAME:
err =
__sys_getsockname(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_GETPEERNAME:
...
進程和系統調用之間是怎么進行的?
進程可以跳轉到的內核中的位置叫做system_call。在此位置的過程檢查系統調用號,它將告訴內核進程請求的服務是什么。然后,它再查找系統調用表sys_call_table,找到希望調用的內核函數的地址,並調用此函數。
gdb 調試lab3的看一看效果
gdb
file vmlinux
break sys_socketcall
target remote:1234
c
list
list
n
n
s 進入__sys_socket
bt #查看堆棧
輸出如下
(gdb) bt
#0 __sys_socket (family=2, type=2, protocol=0) at net/socket.c:1327 #sys_socket 的內容主要就是上面的switch中的結果了
#1 0xc1757b98 in __do_sys_socketcall (args=<optimized out>,
call=<optimized out>) at net/socket.c:2555
#2 __se_sys_socketcall (call=1, args=-1076677360) at net/socket.c:2527 #找到了對應socket類的系統調用函數總入口地址
#3 0xc1002095 in do_syscall_32_irqs_on (regs=<optimized out>) #syscall_trace_enter取出系統調用號 nr;到sys_call_table中去找到nr號對應的系統調用服務程序去執行后返回值放入ax。
at arch/x86/entry/common.c:334
#4 do_fast_syscall_32 (regs=0xc7895fb4) at arch/x86/entry/common.c:397 #做一些額外的設置,里面可是有0x80,然后調用do_syscall_32_irqs_on
#5 0xc199141b in entry_SYSENTER_32 () at arch/x86/entry/entry_32.S:887 #保存現場將相關寄存器中的值壓棧(rax,rsi,rdi等)
#6 0x00000001 in ?? () #上層就是用戶進程了
#7 0xbfd33510 in ?? ()
系統調用咋回事?參考:https://docs.huihoo.com/joyfire.net/6-1.html
更多socke函數的分析 參考: https://blog.csdn.net/zhangskd/category_9263957.html