Linux Kernel 0.12 启动简介,调试记录(Ubuntu1804, Bochs, gdb)

2024-06-16 06:58

本文主要是介绍Linux Kernel 0.12 启动简介,调试记录(Ubuntu1804, Bochs, gdb),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

PS:要转载请注明出处,本人版权所有。

PS: 这个只是基于《我自己》的理解,

如果和你的原则及想法相冲突,请谅解,勿喷。

环境说明
  • Ubuntu 18.04
  • gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
  • Bochs 2.6
  • As86 version: 0.16.17

前言


  自从我近段时间开始温习一些基础知识以来,其中觉得以前学的很浅的就是OS原理。为啥这样说呢?因为就是浅,知道一些琐碎的知识。以前我自负的认为OS就是硬件的抽象,然后把这些硬件资源合理的分配给用户使用就完了,因为我觉得合理的整合这些硬件资源是非常‘简单’的。

  由于我本身对底层是非常着迷的。带着觉得OS很简单的想法,想着去看看LinuxKernel的源码。在以前,我对LinuxKernel的认知很肤浅,就知道一些驱动移植的事情。如果硬要说一件我在LinuxKernel中玩的很深的事情,那就是自己理解并实现了一个类似Anonymous Shared Memory的Linux驱动,详见以下两篇文章。

  • 《Android匿名共享内存(Anonymous Shared Memory) — 瞎折腾记录 (驱动程序篇)》 https://blog.csdn.net/u011728480/article/details/88420467
  • 《linux kernel 中进程间描述符的传递方法及原理》 https://blog.csdn.net/u011728480/article/details/88553602

  带着这样的想法其实已经很久了,由于现在的LinuxKernel太大了,对新手不友好。我就想着去找一个老一点的版本内核看看。结果去网上一找,就发现了前人已经做了许多许多了,比如这个之前就有了解的《linux 0.11内核完全注释》,还比如其他许许多多前人种的‘树’,看到了许多,最终我决定跟着国内现在比较好和新的资料从‘远古’开始学习它。它就是《Linux内核完全注释(PDF) v5.0 by 赵炯.pdf》。它是基于LinuxKernel0.12 讲述的,它是我在ubuntu1804上编译通过LinuxKernel0.12的主要参考和学习资料,同时也是我在Bochs上运行成功的主要参考和学习资料。

  好的多说无益,直接看运行效果。

result_img

  说来也惭愧,利用断断续续的时间,我花了约2月,把LinuxKernel0.12在Ubuntu1804上编译通过,并在1804上通过Bochs运行成功。而且要命的事情是我其实只加了一些打印调试函数,和根据实际的调试情况修改了一些代码,却花了那么久的时间,搞得我很不自信了QAQ。

  我修改好的源码已经开源,立即想要源码的请直接去文末两个rep clone即可。

  本文主要还是简单介绍LinuxKernel从上电到进入sh的中间的简要流程。这些流程网上已经有很多了,可能我会挑选一些我觉得比较重要的来说。

  本文适用于:

  • 会编译和使用bochs的人。不会可以去网上找找,很多这方面的资料。
  • 对Intel AT&T 汇编有点了解的人。
  • 会GDB调试的人。
  • 知道C语言常识的人。
  • 对LinuxKernel感兴趣的人。




搭环境


  工欲善其事必先利其器。本文主要是在Ubuntu1804上编译生成LinuxKernel,然后用Bochs运行我们的内核。



Ubuntu18.04环境安装

我们应该首先安装make,gcc,gcc-multilib,bin86。

  • sudo apt install build-essential cmake make gcc-multilib g+±multilib module-assistant bin86

然后进入源码目录。

  • cd my_src
  • make disk

更多的详情信息查看开源的rep。



编译两个bochs版本备用

  我们首先就得把Linux0.12的运行环境搭建起来,方便我们调试。我们使用的是Bochs2.6 和 GDB远程调试。并编译出两个bochs版本,一个是带本身调试功能(命名为:bochs),一个是和gdb联调(命名为:bochsdbg)。bochs 主要是调试在init/main()函数之前的内容以及查看更多的x86寄存器。 bochsdbg主要是调试进入init/main()函数之后到sh成功执行的事情。

  • 通过 ./configure --enable-debugger 生成bochs。
  • 通过 ./configure --enable-gdb-stub 生成bochsdbg。


运行我们编译的内核

  通过本文介绍生成的文件是Linux内核镜像,稍微懂点行的人都知道还差一个RootFS。这个文件系统我们在网上下载的例如: http://oldlinux.org/Linux.old/bochs/linux-0.12-080324.zip 。本文生成的Linux内核镜像使用的是rootimage-0.12-hd这个文件系统。

  我建议这里自己配置两个.bxrc文件,一个对应bochs,一个对应bochsdbg远程调试。这样在遇到问题的时候我们可以很方便的调试。





LinuxKernel启动简介


  本节简述LinuxKernel的启动流程。根据我近段时间的学习来看,这里包含了许多的历史性的东西,大家不要去细究为啥是这样,很多都是为了兼容。

  此外在整个学习期间,由于涉及到许多的x86 硬件体系知识,除了参考上文我说的文档以外,还必须参考以下Intel官方文档:

  • Intel® 64 and IA-32 architectures software developer’s manual combined volumes 2A, 2B, 2C, and 2D:Instruction set reference, A-Z
  • Intel® 64 and IA-32 architectures software developer’s manual combined volumes 3A,
    3B, 3C, and 3D: System programming guide
  • 《Linux内核完全注释(PDF) v5.0 by 赵炯.pdf》 第4章,全篇精华。


boot/bootsect.S 阶段

  当我们的计算机上电以后,IntelCPU进入实模式,并且PC指向了0xfff0整个地址,如下图。什么意思呢?就是开机的时候执行的第一句指令放在0xffff0这个地方,通常这里有一个很重要的东西叫做BIOS。我们可以看到下图,cs=0xf000,base=0xffff0000,在实模式下面,cs:pc 就是真实的指向地址0xffff0。到了这里不知道大家发现没有,这里还差一个东西,那就是bios本来是放在rom里面的,怎么被指向了内存地址0xffff0的地方呢?是谁在之前自动搬运的吗?经过查询后发现,大部分人说开机的时候,对特殊地址的访问会被仲裁器件指向BIOS-ROM器件。仲裁器还可以把地址翻译并指向我们熟悉的MEM和IO。所以这里我理解对0xffff0的访问就是对BIOS-ROM器件的直接访问和执行。

poweron_img

  BIOS主要是做自检,并且在物理地址0x0开始初始化BIOS的中断向量,同时通过BIOS访问存储设备的中断,将可启动设备的第一个扇区512字节给搬运到绝对地址0x7c00(31k)处。然后跳转到0x7c00继续执行,这里被搬运的512字节就是bootsect.S生成的指令。这一段没啥营养,都是一些约定好的,到了CPU执行到绝对地址0x7c00的时候,才是真正的我们能控制的地方。其实这里也能够看到,我们的bootsect.S生成的指令最大只能够512字节,超过了就会出问题。下图为我们的0x7c00处的开始几句指令和bootsect.S的几句指令,同时也能够看到BIOS初始化和自检打印的一些内容:

7c00_img
  在上图的图中,我打印了0x7c00开始的一部分反汇编代码。可以看到和下面的bootsect.S的代码是一致的。
entry start
start:
! start at 0x07c0:0
! add by skymov ax,#BOOTSEGmov es,axmov	bp,#msg2	  ! sky-notes: src-str is es:bpmov	si,#15        ! sky-notes: src-str-len is cxcall pirnt_str
! add by skymov	ax,#BOOTSEGmov	ds,axmov	ax,#INITSEGmov	es,axmov	cx,#256sub	si,sisub	di,direpmovwjmpi	go,INITSEG

  从0x7c00开始,就是我们自己的可以编程的领域了,也开始有了一些我自己特有的内容。主要是各种方法实现的print语句。这种调试方法简直不要太好。

  下面简要说明一下bootsect.S的功能:

  • 首先用rep movw把自己从0x7c00搬运到0x90000,并跳转cs=0x9000, pc=go 标号的地址。继续执行剩下的内容。
  • 通过读取0x1E号中断向量位置的软驱参数(由BIOS初始化时候通过BIOS中断读取的)到内存,然后修改其中的最大扇区数,并重新写回到0x1E中断向量位置绝对地址0x78去。最后重置软驱,使其加载最新的参数。
  • 使用BIOS INT 0x13的2号功能,将第一个软盘第2,3,4,5扇区读取到0x90200开始的位置。这里读取的就是setup.S的指令内容,最大共2k(4*512)。0x90000-0x90200存放的是bootsect.S, 0x90200-0x90A00 为setup.S。
  • 使用BIOS INT 0x13的8号功能,读取磁盘参数:每磁道扇区数。并保存到变量sectors中。
  • 使用BIOS INT 0x13的2号功能,使用刚刚的参数,读取system模块到0x10000,我们的bootsect.S放在0x90000,所以我们system模块最大只能够占用0x10000~0x8ffff。这里的system模块就是除了bootsect和setup模块之外的所有内核代码。
  • 判断bootsect模块第508,509字节是否为0,来判断我们是否指定根文件系统的设备号。我们的内核定义为0x0301,代表第一个磁盘第一个分区为我们的根文件系统。
  • 然后通过jmpi 0:9020跳转到cs=0x9020,pc=0的地方去执行setup.S的代码。

  在我的bootsect模块,我定义了一个打印字符串的函数,主要是通过使用BIOS INT 0x10的0x13号功能实现。主要还是为了调试,注意,这里不能够随意添加代码,因为生成的代码超过512byte后,链接器会报错。只能够少量的添加我们的调试代码。

  至此,我们就执行完了bootsect模块。本模块的主要内容还是加载setup和system到指定位置。bootsect执行的一些调试日志如下图(在0x90200下断点):

bootsect_log_img
注意:图中话框的部分就是我们上文贴出的call pirnt_str打印的。

boot/setup.S 阶段

  首先我们还是来看一下0x90200的位置是否是setup.S,换句话来说是否加载好了setup模块。

90200_img
  这里和bootsect一样,我也弄了一个prtstr函数,这个prtstr和bootsect里面的是一样的,原理也是一致的。

  刚刚我们提到,setup是从0x90200开始存放的。那么0x90000~0x901ff中的bootsect已经无用了,于是我们setup中,用这里的内存存放一些参数。下面简要说明一下setup.S的功能:

  • 用BIOS INT 0x15功能号0x88取系统所含扩展内存大小并保存在内存0x90002~0x90003处。共两个字节。
  • 用BIOS INT 0x10功能号0x12读取显卡参数,0x9000A 显存大小,0x9000B 显卡类型(单色/彩色),0x9000C显卡特性参数。
  • 用BIOS中断读取屏幕的行列存放到0x9000E 0x9000F
  • 用BIOS INT 0x10功能号0x03读取当前光标位置存放到0x90000 0x90001
  • 用BIOS INT 0x10功能号0x0f读取当前显示页,显示模式,字符列数。 0x90004~0x90005 存放当前显示页。 0x90006 显示模式, 0x90007 字符列数。
  • 读取第一个硬盘参数表和第二个硬盘参数表,并放到0x90080 0x90090。每个表共16byte。注意,这里和之前的软盘参数一样,在BIOS自检过程中,就被放到了中断向量0x41 0x46 的位置。
  • 用BIOS INT 0x13功能号0x15读取当前硬盘设备情况,如果硬盘2不存在,则把0x90090之后的16byte清零。

  下面我们将使CPU从实模式变更为保护模式,下面继续说明一下setup.S的功能:

  • 禁用中断。
  • 然后我们把system模块0x10000~0x8ffff整体下移到0x0开始的位置。就是把最大0x80000(512k)的system模块向下移动0x10000(64k)。
  • 首先加载LDT和GDT。
  • 开启A20地址线,支持1M以上的内存。
  • 初始化两个8259A中断控制器。
  • 通过lmsw 设置cr0最低位位1,进入保护模式。
  • 通过jmpi 0:0x8跳转到绝对地址0x0开始执行system的代码。system是从boot/head.s开始的。

  这里需要说明几个事情:

  • 我们在下移system模块的时候,覆盖了BIOS中断向量表。所以通过BIOS中断打印字符串是行不通的。
  • 在实模式中,cs:pc就是真实执行的地址。但是在保护模式中,cs是一个选择符号,根据选择符号值不同,分表在GDT或者LDT中查找对应的CS段描述符,其中最重要的就是base地址,当未开启分页的时候,这里的base+pc就是我们真实的执行地址。上面我们加载了LDT和GDT。这里的LDT是空,GDT有3项,第零项是空,第一项是代码段描述符,第二项是数据段描述符,他们的基地址都是0x0。当cs=0x08,ds=0x10时,分别指向这里的第一项和第二项。

  刚刚说了,system下移导致BIOS中断向量表被冲掉了,于是我们不能够通过BIOS打印字符串,于是这里我们使用的是直接操作显存内存地址显示字符,这个原理和LinuxKernel tty显示原理差别不是很大。

  这里我们设计了print_str函数,通过直接操控显存然后写入字符进行显示,这里还使用到了刚刚我们保存的当前光标位置(0x90000 0x90001)。写这个主要还是为了调试。

  到此,我们已经开始去执行system的内容,其中head.s是入口。下图是在0x0下断点得到的setup模块的一些打印日志。

setup_log_img
  这里我们可以看到,红框还是BIOS中断打印的,黄框是通过直接操纵显存显示的。注意,我这里设计的直接操作显存的函数,是通过循环在当前显存页显示的,并不是我们常见的整页上移的方式。

boot/head.s 阶段

  首先我们还是来看一下0x0的位置是否是head.s,换句话来说是否加载好了system模块。并且,从这里开始,我们就是进入了真正的LinuxKernel的世界,前面都是做一些环境初始化,都是一些固定的内容。

head_start_img

  这里我们需要说明的是,bootsect.S和setup.S用的是intel汇编,而从head.s开始,我们用的都是AT&T汇编。同理,这里我也弄了一个safe_mode_print_str_no_page,打印字符串,为了调试,还是用的直接操作显存的方式。

  从这里开始,CPU开始工作于保护模式,下面简要介绍一下工作流程:

  • 刚刚我们通过jmpi切换到0x0开始执行,这时cs=0x8,根据setup设置好的GDT,base为0x0,同理我们设置其他段寄存器。
  • 设置堆栈为stack_start,这个就是内核堆栈。此符号定义于kernel/sched.c中,如下文。
long user_stack [ PAGE_SIZE>>2 ] ;struct {long * a;short b;// } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };} stack_start = { & user_stack , 0x10 };
  • 设置IDT,所有的中断向量指向ignore_int,一个预定义的中断服务程序。共256项,每项8byte。
  • 重新设置GDT。共256项,每项8byte。重新设置GDT的原因是setup的GDT可能会被冲掉,于是把GDT设置到合理的内存位置。这里设置好的GDT有4个。和setup中类似。第0,3个为0.第1,2项为cs和ds的段描述符。
  • 检查A20是否开通,主要是通过判断0x100000 和 0x0值是否相等。
  • 检查数学协处理器是否存在。

  到这里,我们就开始准备正式进入到init/main.c中的main函数了,但是还差最后一个重要的事情,那就是启用分页机制,下面继续介绍其工作流程:

after_page_tables:# sky printpush %ebplea msg5, %ebpcall safe_mode_print_str_no_pagepop %ebp	#pushl $0		# These are the parameters to main :-)pushl $0pushl $0pushl $L6		# return address for main, if it decides to.pushl $mainjmp setup_paging
  • 从上面的代码我们可只,我们在启用分页前,把init/main.c中的main函数地址设置到了堆栈中。
  • 首先我们把从0x0开始的5页内存清零。每页4096字节。其中第一页为页表目录,第2-5页为页表。
  • 设置页表目录的前4项为第2-5页页表地址。注意页表目录为1024项,每项4字节。
  • 倒序设置每一个页表的每一项内容,第5页最后一项为0xfff000。映射之后,2-5页分别映射好了16MB内存的空间。
  • 操作cr0,开启分页
  • 通过ret指令,从堆栈中把main地址弹出去执行。

  到这里,我们正式进入到init/main.c中的main函数中,进入c语言相关代码的地界。下面是进入main之前的一些日志输出。

head_log_img


init/main.c 到进入shell

  这里我们进入了init/main.c中的main函数,可从下图看到。从这里开始,也是我们大家都熟知的Linux内核部分。

init_main_img
void main(void)		/* This really IS void, no error here. */
{			/* The startup routine assumes (well, ...) this */
/** Interrupts are still disabled. Do necessary setups, then* enable them*/char _my_msg_buf[100];sprintf(_my_msg_buf, "kernel main() start, root_dev=%x, swap_dev=%x ... ...\0", ORIG_ROOT_DEV, ORIG_SWAP_DEV);__asm__ ("push %%ebp\n\t""mov %0, %%ebp\n\t""call safe_mode_print_str_after_page\n\t" "pop %%ebp\n\t"::"p"((char *)&_my_msg_buf):);ROOT_DEV = ORIG_ROOT_DEV;SWAP_DEV = ORIG_SWAP_DEV;sprintf(term, "TERM=con%dx%d", CON_COLS, CON_ROWS);envp[1] = term;	envp_rc[1] = term;drive_info = DRIVE_INFO;memory_end = (1<<20) + (EXT_MEM_K<<10);memory_end &= 0xfffff000;//align 4kif (memory_end > 16*1024*1024)//if memory_end > 16MB, set it to be 16 MBmemory_end = 16*1024*1024;if (memory_end > 12*1024*1024) buffer_memory_end = 4*1024*1024;else if (memory_end > 6*1024*1024)buffer_memory_end = 2*1024*1024;elsebuffer_memory_end = 1*1024*1024;main_memory_start = buffer_memory_end;sprintf(_my_msg_buf, "Mem size is %x, buf-mem size is %x, main-mem start %x ... ...\0", memory_end, main_memory_start, buffer_memory_end);__asm__ ("push %%ebp\n\t""mov %0, %%ebp\n\t""call safe_mode_print_str_after_page\n\t" "pop %%ebp\n\t"::"p"((char *)&_my_msg_buf):);#ifdef RAMDISKsprintf(_my_msg_buf, "ramdisk init ... ...\0");__asm__ ("push %%ebp\n\t""mov %0, %%ebp\n\t""call safe_mode_print_str_after_page\n\t" "pop %%ebp\n\t"::"p"((char *)&_my_msg_buf):);main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endifsprintf(_my_msg_buf, "memory init ... ...\0");__asm__ ("push %%ebp\n\t""mov %0, %%ebp\n\t""call safe_mode_print_str_after_page\n\t" "pop %%ebp\n\t"::"p"((char *)&_my_msg_buf):);mem_init(main_memory_start,memory_end);sprintf(_my_msg_buf, "trap init ... ...\0");__asm__ ("push %%ebp\n\t""mov %0, %%ebp\n\t""call safe_mode_print_str_after_page\n\t" "pop %%ebp\n\t"::"p"((char *)&_my_msg_buf):);trap_init();sprintf(_my_msg_buf, "blk init ... ...\0");__asm__ ("push %%ebp\n\t""mov %0, %%ebp\n\t""call safe_mode_print_str_after_page\n\t" "pop %%ebp\n\t"::"p"((char *)&_my_msg_buf):);blk_dev_init();sprintf(_my_msg_buf, "chr init ... ...\0");__asm__ ("push %%ebp\n\t""mov %0, %%ebp\n\t""call safe_mode_print_str_after_page\n\t" "pop %%ebp\n\t"::"p"((char *)&_my_msg_buf):);chr_dev_init();sprintf(_my_msg_buf, "tty init ... ...\0");__asm__ ("push %%ebp\n\t""mov %0, %%ebp\n\t""call safe_mode_print_str_after_page\n\t" "pop %%ebp\n\t"::"p"((char *)&_my_msg_buf):);tty_init();printk("time init ... ...\n\r");time_init();printk("sched init ... ...\n\r");sched_init();/*After sched_init()gdt[0] = NULLgdt[1] = kernel csgdt[2] = kernel dsgdt[3] = NULLgdt[4] = task0.tssgdt[5] = task0.ldttr=task0.tssldtr=task0.ldt*/printk("buffer init ... ...\n\r");buffer_init(buffer_memory_end);printk("hd init ... ...\n\r");hd_init();printk("floppy init ... ...\n\r");floppy_init();printk("enable interrupts ... ...\n\r");sti();printk("go to user mode ... ...\n\r");/*movl %%esp,%%eaxpushl $0x17pushl %%eaxpushflpushl $0x0fpushl $1firet1:movl $0x17,%%eaxmov %%ax,%%dsmov %%ax,%%esmov %%ax,%%fsmov %%ax,%%gsiret instruction will do follow op:popl eippopl cspopl eflagpopl esppopl ss*/move_to_user_mode();printf("user_mode: fork() task0 ... ...");if (!fork()) {		/* we count on this going ok */printf("user_mode: task1 call init ... ...");init();}
/**   NOTE!!   For any other task 'pause()' would mean we have to get a* signal to awaken, but task0 is the sole exception (see 'schedule()')* as task 0 gets activated at every idle moment (when no other tasks* can run). For task0 'pause()' just means we go check if some other* task can run, and if not we return here.*/printf("user_mode: task0 call sys_pause() in while ... ...");for(;;)__asm__("int $0x80"::"a" (__NR_pause):);
}

  注意,这里我们仍然设计了一个函数为safe_mode_print_str_after_page,通过直接操作显存进行显示字符串,知道tty_init之后,我们才能够调用printk类似的函数进行打印。

  下面简要介绍一下main函数主要做的事情:

  • 根据我们在setup中保存到内存中的内存参数初始化高速缓冲区和主存的位置。
  • 然后就是我们常见的初始化mm模块。
  • 初始化中断向量。
  • 初始化块设备。
  • 初始化字符串设备。
  • 初始化tty设备。
  • 初始化时间。
  • 初始化调度模块。
  • 初始化缓冲区。
  • 初始化硬盘。
  • 初始化软盘。
  • 开启中断。
  • 把当前任务切换到用户态。

  当我们切换到用户态之后,并且当前我们的进程是0号进程,我们内核的一些重要初始化基本设置完毕。然后就像我们常见的linux编程那样,通过fork,创建我们的1号进程。然后我们继续进行下面的事情:

  • task0在fork出task1之后,就循环调用sys_pause, 这里主要还是执行schedule()开始执行进程调度。
  • task1成功创建后,调用setup,开始加载根文件系统。然后task1 通过fork创建了task2。
  • task2通过execve开始运行/bin/sh,进入shell。后续就是一些其他的事情。

  到这里,我们已经把kernel跑起来了。在我调试的过程中,主要还是mm模块和schedule模块有些问题,可能和编译器版本有关系,反正我生成的代码,总会报错。哪怕到现在,我开源出来的我修改的内核,也非常的不稳定,经常崩溃。但是好在正常工作了。

  下面给出两种不同打印的日志:

main_log0_img
main_log1_img


tool/build.c

  此工具是生成LinuxKernel镜像的手段。但是我们在Ubuntu上生成的内核,由于gcc版本变更的原因,需要做一些变更。主要还是把生成的elf格式system模块通过objcopy 生成二进制内存镜像。主要原因就是elf格式需要一个elf加载器进行各个段的重定位,但是由于我们是内核,所以没有。详情,请查看tool/build.c 及 Makefile。





开源


  https://github.com/flyinskyin2013/LinuxKernel-src0.12

  https://gitee.com/sky-X/LinuxKernel-src0.12 (镜像)





后记


  为啥想要在ubuntu1804环境下弄这个东西呢?一方面是想学习一下,通过踩坑的方式加深自己的理解。另一方面还是太懒了,我只想在我的ubuntu1804上编译内核,不想安装其他虚拟机了,我的电脑太卡了(毕竟8年的电脑了QAQ)。

  经过了这一波调试,我对LinuxKernel有了更深的认知,我觉得很不错,如果以后有必要,我还可以分别对这些模块进行详细的查看,在这里,我只是简单的说明了init/main中的内容,其实,还有许多其他的内容是运行在背后的。比如system_call,sys_table等等内容。还有do_fork do_execve等等内容都是我在调试过程中踩过的坑。

  这里还是要说明,深入调试学习这个的原因还是想看看OS是怎么运行起来,虽然不能说已经100%的熟知,但是也可管中窥豹。

  注意,这个版本的内核和现代的2.0,4.0,5.0还缺了一些主要的知识,比如网络栈,VFS等。但是其他的一些内容,在现在的最新内核中,多多少少都能够看到这个版本的一些影子。这也是学习这个内核的原因之一。




打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)
qrc_img

PS: 请尊重原创,不喜勿喷。

PS: 要转载请注明出处,本人版权所有。

PS: 有问题请留言,看到后我会第一时间回复。

这篇关于Linux Kernel 0.12 启动简介,调试记录(Ubuntu1804, Bochs, gdb)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1065755

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

MySQL数据库宕机,启动不起来,教你一招搞定!

作者介绍:老苏,10余年DBA工作运维经验,擅长Oracle、MySQL、PG、Mongodb数据库运维(如安装迁移,性能优化、故障应急处理等)公众号:老苏畅谈运维欢迎关注本人公众号,更多精彩与您分享。 MySQL数据库宕机,数据页损坏问题,启动不起来,该如何排查和解决,本文将为你说明具体的排查过程。 查看MySQL error日志 查看 MySQL error日志,排查哪个表(表空间

springboot3打包成war包,用tomcat8启动

1、在pom中,将打包类型改为war <packaging>war</packaging> 2、pom中排除SpringBoot内置的Tomcat容器并添加Tomcat依赖,用于编译和测试,         *依赖时一定设置 scope 为 provided (相当于 tomcat 依赖只在本地运行和测试的时候有效,         打包的时候会排除这个依赖)<scope>provided

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

内核启动时减少log的方式

内核引导选项 内核引导选项大体上可以分为两类:一类与设备无关、另一类与设备有关。与设备有关的引导选项多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导选项。比如,如果你想知道可以向 AHA1542 SCSI 驱动程序传递哪些引导选项,那么就查看 drivers/scsi/aha1542.c 文件,一般在前面 100 行注释里就可以找到所接受的引导选项说明。大多数选项是通过"_

ASIO网络调试助手之一:简介

多年前,写过几篇《Boost.Asio C++网络编程》的学习文章,一直没机会实践。最近项目中用到了Asio,于是抽空写了个网络调试助手。 开发环境: Win10 Qt5.12.6 + Asio(standalone) + spdlog 支持协议: UDP + TCP Client + TCP Server 独立的Asio(http://www.think-async.com)只包含了头文件,不依

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

用命令行的方式启动.netcore webapi

用命令行的方式启动.netcore web项目 进入指定的项目文件夹,比如我发布后的代码放在下面文件夹中 在此地址栏中输入“cmd”,打开命令提示符,进入到发布代码目录 命令行启动.netcore项目的命令为:  dotnet 项目启动文件.dll --urls="http://*:对外端口" --ip="本机ip" --port=项目内部端口 例: dotnet Imagine.M