Linux内核启动(3,0.11版本)内核启动完成与进入内核main函数

news2024/11/18 18:19:32

这一部分是在讲解head.s代码,这个代码与bootsect.ssetup.s在同一目录下,但是head.s程序在被编译生成目标文件后会与内核其他程序一起被链接成system模块,位于system模块的最前面开始部分。system模块将被放置在磁盘上setup模块之后开始的扇区中,即从磁盘上第6个扇区开始放置。其大小为120KB,其中head部分有14KB,head除了做了一些调用main函数的准备工作之外,还做了一件对内核程序在内存中的布局已经对内核程序的正常运行都有重大意义的事情,就是用程序自身代码在程序自身所在的内存空间中创建了内核分页机制,即在 0x00000000处创建了页目录表,页表,缓冲区,GDT,IDT,并将head程序已经执行过的代码所占空间覆盖,这意味着head程序自己将自己废弃,main函数开始执行。我们看看这些是怎么实现的。

同时,CPU的运行模式变了,汇编语法也变了,变成了比较麻烦的AT&T语法,对于AT&T语法不是很熟悉的可以参考别的博客

开始head.s

pg_dir:
.globl startup_32
startup_32:
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
	call setup_idt
	call setup_gdt
	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
	xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b

pg_dir标识内核分页机制完成后的内核起始位置,也就是物理内存的起始位置,head程序马上要在这里建立页目录表,为分页机制做准备,这一点非常重要,是内核能够掌握用户进程的基础之一,其占据的内存空间为 0x0000-0x4FFF,在实模式下CS是代码段基址,但是在保护模式下CS是代码段选择符,之后在设置分页机制时,页目录会存放在这里,也会覆盖这里的代码。

    jmpi 0,8

这个代码使得CS与GDT的第二项关联,并且使代码段基址指向 0x000000。所以这个指令是一样的

可以看到mov指令来初始化段寄存器ds/es/fs/gs,指向可读可写但不能执行的数据段。(这个是怎么指向数据段的呢,我也是一段时间之后才反应过来,他是类似于上面的指令,我们需要明白的一点是什么呢,现在是保护模式了,所以段寄存器不是直接指向内存了,我们需要通过GDT来访问内存了,通过GDT访问内存,这点非常重要非常重要)

在CPU中,跟段有关的CPU寄存器一共有6个:cs,ss,ds,es,fs,gs,它们保存的是段选择符。而同时这六个寄存器每个都有一个对应的非编程寄存器,它们对应的非编程寄存器中保存的是段描述符。

CS:代码段寄存器

DS:数据段寄存器

SS:栈段寄存器

ES:扩展段寄存器

FS:GS:386处理器之后被引入

这里初始化的目的是为了让着四个寄存器可以变到保护模式,然后加载堆栈段描述符,我们来看看stack_start到底是啥:

long user_stack [ PAGE_SIZE>>2 ] ;

struct {
    long * a;
    short b;
    } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

这个程序在/kernel/sched.c文件当中。lss作用在这里最终的效果是把0x10作为段选择子加载到ss中,并将user_stack的地址放到esp中。可以测算出其起始位置为 0x1E25Clss指令相当于让 ss:esp 这个栈顶指针指向了 _stack_start 这个标号的位置。还记得原来的栈顶指针在哪里嘛?0x9FF00,现在要变了。在这个结构体中,高位 8 字节是 0x10,将会赋值给 ss 栈段寄存器,低位 16 字节是 user_stack 这个数组的最后一个元素的地址值,将其赋值给 esp 寄存器。为什么是最后一个元素值呢,因为栈是从后往前生长的。

0x10我们这里也应该当做段选择子来看待,其实就是第四个段选择符,段基址为 0x000000,段限长8MB,内核特权级,后面的压栈动作就在这里执行。

这里我们就能看出来了,实模式和保护模式的寻址方式差异非常大,如果没有我们之前在实模式下创建的GDT表格,现在寻址都无法执行,我们之前设置栈的寄存器是SP,现在是ESP,这是专为保护模式下进行操作作出的调整。

setup_idtsetup_gdt分别对应建立新的IDT表和GDT

建立新的IDT

/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It then loads
 *  idt. Everything that wants to install itself
 *  in the idt-table may do so themselves. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok. This routine will be over-
 *  written by the page tables.
 */
setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea idt,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr
	ret

ignore_int是一个函数,作用是打印Unknown interrupt这个字符串,然后结束。想看看的给你瞅一眼:

我们还是先解释清除这段代码写了什么,lea这个指令可以将ignore_int的偏移地址给edx,然后将0x00080000这个值给了eax

%eax,可存放一般数据,可作为累加器使用;
%ebx,可存放一般数据,可用来存放数据的指针(偏移地址);
%ecx,可存放一般数据,可用来做计数器,常常将循环次数用它来存放;
%edx,可存放一般数据,可用来存放乘法运算产生的部分积,用来存放输入输出的端口地址(指针);
%esi,可存放一般数据,可用于串操作中,存放源地址,对一串数据访问;
%edi,可存放一般数据,可用于串操作中,存放目的地址,对一串数据访问;
%esp,用于寻址一个称为堆栈的存储区,通过它来访问堆栈数据;
%ebp,可存放一般数据,用来存放访问堆栈段的一个数据区,作为基地址;

MOVB,MOVW,MOVL三种指令的区别是它们分别是在大小为 1,2和4个字节的数据上进行操作。

MOVW 把 16 位立即数放到寄存器的底16位,高16位清0
MOVT 把 16 位立即数放到寄存器的高16位,低 16位不影响

/* This is the default interrupt "handler" :-) */
int_msg:
	.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg
	call printk
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret

printk是一个函数,被定义在linuxsrc/kernel/printk.cprintk函数。

我们看一下中断描述符的组成

请添加图片描述

Offset:对应中断服务程序的段内偏移地址

Selector:所在段选择符

DPL:描述符特权级

P:段存在标志

TYPE:段描述符类型

创建IDT表是重建保护模式下中断服务体系的开始,程序先让所有的中断描述符默认指向ignore_int这个位置(将来main函数里面还要让中断描述符对应具体的中断服务程序),之后还要对IDT寄存器的值进行设置,这种初始化操作,可以防止无意中覆盖代码或数据引起的逻辑混乱,以及对开发中的误操作给出及时的提示。IDT有256个表项,实际只使用了几十个,对于误用,未使用的中断描述符,这样的提示信息可以提醒开发人员注意错误。

实际上这段程序的作用就是设置中断描述符表,中断嘛,对应着中断号,你发起中断,给中断号,执行中断程序,此时所有的中断号都指向了一个打印程序,hhhh,这个打印程序就是默认的中断处理程序,之后会被重新设置的中断覆盖。

建立新的GDT

/*
 *  setup_gdt
 *
 *  This routines sets up a new gdt and loads it.
 *  Only two entries are currently built, the same
 *  ones that were built in init.s. The routine
 *  is VERY complicated at two whole lines, so this
 *  rather long comment is certainly needed :-).
 *  This routine will beoverwritten by the page tables.
 */
setup_gdt:
	lgdt gdt_descr
	ret

这个是构造好了的

.align 2
.word 0
idt_descr:
	.word 256*8-1		# idt contains 256 entries
	.long idt
.align 2
.word 0
gdt_descr:
	.word 256*8-1		# so does gdt (not that that's any
	.long gdt		# magic number, but it works for me :^)

	.align 8
idt:	.fill 256,8,0		# idt is uninitialized

gdt:	
	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	    /* 16Mb */
	.quad 0x00c0920000000fff	    /* 16Mb */
	.quad 0x0000000000000000	    /* TEMPORARY - don't use */
	.fill 252,8,0			        /* space for LDT's and TSS's etc */

最后的gdt:就是gdt表的样子,其实吧和我们之前设置的gdt表一模一样,只是换个位置。第零段位空,第一段指向代码段,第二段指向数据段,第三段为空,剩余的252项留给任务状态段描述符TSS和局部描述符LDT

这里我们搞好了之后

    movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
	xorl %eax,%eax

然后又来了一遍加载,每次更新GDT之后,由于段描述符的变化,我们必须重新加载一遍,保证与最新的保持一致。

1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b

这部分开始检查A20是否真正的开启了,防止出了差错,否则就一直循环。

/*
 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
 * mode. Then it would be unnecessary with the "verify_area()"-calls.
 * 486 users probably want to set the NE (#5) bit also, so as to use
 * int 16 for math errors.
 */
	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
	orl $2,%eax		# set MP
	movl %eax,%cr0
	call check_x87
	jmp after_page_tables

这段代码就是检查数字协处理器芯片是否存在。这个和硬件相关,确认无误后跳转

after_page_tables:
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $main
	jmp setup_paging
L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.

到这里,我们开始压栈,压栈完毕后,然后跳转到setup_paging

开启分页

/*
 * Setup_paging
 *
 * This routine sets up paging by setting the page bit
 * in cr0. The page tables are set up, identity-mapping
 * the first 16MB. The pager assumes that no illegal
 * addresses are produced (ie >4Mb on a 4Mb machine).
 *
 * NOTE! Although all physical memory should be identity
 * mapped by this routine, only the kernel page functions
 * use the >1Mb addresses directly. All "normal" functions
 * use just the lower 1Mb, or the local data space, which
 * will be mapped to some other place - mm keeps track of
 * that.
 *
 * For those with more memory than 16 Mb - tough luck. I've
 * not got it, why should you :-) The source is here. Change
 * it. (Seriously - it shouldn't be too difficult. Mostly
 * change some constants etc. I left it at 16Mb, as my machine
 * even cannot be extended past that (ok, but it was cheap :-)
 * I've tried to show which constants to change by having
 * some kind of marker at them (search for "16Mb"), but I
 * won't guarantee that's all :-( )
 */
.align 2
setup_paging:
	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */
	cld;rep;stosl
	movl $pg0+7,pg_dir		    /* set present bit/user r/w */
	movl $pg1+7,pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,pg_dir+12		/*  --------- " " --------- */
	movl $pg3+4092,%edi
	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */
	std
1:	stosl			        /* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax
	jge 1b
	xorl %eax,%eax		/* pg_dir is at 0x0000 */
	movl %eax,%cr3		/* cr3 - page directory start */
	movl %cr0,%eax
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			        /* this also flushes prefetch-queue */

这些代码会让改Linux内核向现代操作系统更近了一步,开启分页保护。分页的情况如下:

/*
 * I put the kernel page tables right after the page directory,
 * using 4 of them to span 16 Mb of physical memory. People with
 * more than 16MB will have to expand this.
 */
.org 0x1000
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000

我们回顾一下分页的一些知识,开启分页需要将CR0寄存器中的PG位开启,开启这个标志之前必须已经或者同步开启 PE标志位,PG = 0PE = 0,处理器工作在实地址模式下。PG = 0PE = 1,处理器工作在没有开启分页机制的保护模式下。PG = 1PE = 0,在PE没有开启的情况下无法开启PGPG = 1PE = 1,处理器工作在开启了分页机制的保护模式下。

我们没开启分页模式之前,我们是怎么找到一个物理地址呢,段寄存器和从全局描述符表中取出段基地址,然后加上偏移地址得到真实的物理地址,但是开启分页机制之后,又会多一步转换,分段机制得到线性地址之后还需要多一步分页机制的转换,要是不开启分页机制的话,这一步就直接是物理地址了。

比如我们的线性地址是 0000000011_0100000000_000000000000 (经过了段机制的转换),那从这个线性地址到实际物理地址的转换是怎么样的呢?

请添加图片描述

高 10 位负责在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是最终的物理地址。这一切的操作都由计算机的一个硬件角MMU,也叫内存管理单元,或者分页内存管理单元这个部件来讲虚拟地址转换为物理地址。整个过程我们是不需要关心的,因为这个是硬件层的操作了,操作系统只是在软件层。

我们的这种分配方式为二级页表,一层页目录项PDE,一层叫页表项PTE,之后再开启分页机制的开关。

所以这段代码,就是帮我们把页表和页目录表在内存中写好,之后开启 cr0 寄存器的分页开关,仅此而已。

按照当时的Linux的任务,总可使用的内存不超过16MB,也就是最大地址空间为 0xFFFFF 。而按照当前的页目录表和页表这种机制,1 个页目录表最多包含 1024 个页目录项(也就是 1024 个页表),1 个页表最多包含 1024 个页表项(也就是 1024 个页),1 页为 4KB(因为有 12 位偏移地址),因此,16M 的地址空间可以用 1 个页目录表 + 4 个页表搞定。

这一段代码还包括将页目录表放在内存地址的最开头,记得pg_dir这个标签嘛,现在就是将页目录表放在了这个位置,也就是内存的一开头的位置,然后紧接着他放四个页表,在页目录表和页表中填写好数值来覆盖16MB的内存,随后开启分页机制。

同时,如 idt 和 gdt 一样,我们也需要通过一个寄存器告诉 CPU 我们把这些页表放在了哪里,就是这段代码。

xor eax,eax
mov cr3,eax

具体的页表设置好后,映射内存的情况时怎样呢,那就看页表的具体数据

setup_paging:
	...
	cld;rep;stosl
	movl $pg0+7,pg_dir		/* set present bit/user r/w */
	movl $pg1+7,pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,pg_dir+12		/*  --------- " " --------- */
	movl $pg3+4092,%edi
	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */
	std
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax
	jge 1b

前五行表示,页目录表的前 4 个页目录项,分别指向 4 个页表。比如页目录项中的第一项 [eax] 被赋值为 pg0+7,也就是 0x00001007,根据页目录项的格式,表示页表地址为 0x1000,页属性为 0x07 表示改页存在、用户可读写。

后面几行表示,填充 4 个页表的每一项,一共 4*1024=4096 项,依次映射到内存的前 16MB 空间。

现在只有四个页目录项,也就是将前面的16M的线性地址空间与16M的物理地址空间对应上了。

具体的分段与分页的理论我们这里就不在详细的展开了,如果需要详细的理论分析可以看我的另一篇文章,详细解释了分段和分页理论。

为什么要重设GDT

为什么要废除原来的GDT而重新设计一套GDT呢?别以为有啥高深的原因,就是因为原来GDT所在的位置是设计代码时再setup.s里面设置的数据,将来这个setup.s 所在的内存位置会在设计缓冲区时被覆盖掉,如果不改变位置,将来GDT的内容就会被覆盖掉,其实吧,就是管理内存,懂了吧

目前的内存分布

内存位置内容
0x00000 - 0x00FFF页目录表
0x01000 - 0x01FFF页表0
0x02000 - 0x02FFF页表1
0x03000 - 0x03FFF页表2
0x04000 - 0x04FFF页表3
0x05000 - 0x05400软盘缓冲区
0x05401 - 0x054B7
0x054B8 - 0x05CB7中断描述符表IDT
0x05CB8 - 0x064B7全局描述符表GDT

我们上面做了那么多工作,其实都是为了达到现在的内存状态啊

准备进入main

我们上面讲了分页代码,这里有一个骚操作帮助进入main函数

after_page_tables:
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $main
	jmp setup_paging
L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.

分页之后我们执行了这个代码,这个代码就有意思了,push指令是压榨,五个push指令过去以后,栈会变成这个样子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XSud4WfK-1676125712644)(C:\Users\LoveSS\AppData\Roaming\Typora\typora-user-images\image-20230211212519126.png)]

然后注意,setup_paging 最后一个指令是 ret,也就是我们上一回讲的设置分页的代码的最后一个指令,形象地说它叫返回指令,但 CPU 可没有那么聪明,它并不知道该返回到哪里执行,只是很机械地把栈顶的元素值当做返回地址,跳转去那里执行。

再具体说是,把 esp 寄存器(栈顶地址)所指向的内存处的值,赋值给 eip 寄存器,而 cs:eip 就是 CPU 要执行的下一条指令的地址。而此时栈顶刚好是 main.c 里写的 main 函数的内存地址,是我们刚刚特意压入栈的,所以 CPU 就理所应当跳过来了。

call指令会自动将EIP的值压栈,保护返回现场,然后执行被调函数程序,等到被调函数程序执行完毕后,也就是被调函数ret指令时,自动出栈给EIP并还原现场,继续执行call的下一条指令,对操作系统而言,这个指令就有点怪异了,如果call调用了操作系统的main函数,那ret返回给谁呢?难道还有一个更底层的程序接受操作系统的返回嘛,操作系统已经是最底层的系统了,那还怎么更底层呢,所以这里使用了这么一个技巧,直接将main压栈,ret返回时直接就执行main函数了,而其他的三个压栈的 0,本意是作为 main 函数的参数,但实际上似乎也没有用到,所以也不必关心。

终于终于我们的底层搞完了,进入main函数了,这三章内容讲了我们是怎么从加电到加载完内核并跳到内核程序的,我们梳理一下

加载内核的流程

请添加图片描述

目前的内存分布

内存位置内容
0x00000 - 0x00FFF页目录表
0x01000 - 0x01FFF页表0
0x02000 - 0x02FFF页表1
0x03000 - 0x03FFF页表2
0x04000 - 0x04FFF页表3
0x05000 - 0x05400软盘缓冲区
0x05401 - 0x054B7
0x054B8 - 0x05CB7中断描述符表IDT
0x05CB8 - 0x064B7全局描述符表GDT
0x064B8 - 0x80000system内核
0x80001 - 0x8FFFF
0x90000 - 0x901FCsetup在内存中保存的信息
0x901FD - 0x901FF
0x90200 - 0x90A00setup程序
0x90A00 - 0x9FF00栈(栈指向0x9FF00),并未占完
0x9FF01 - 0xFDFFF

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/338492.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文阅读:《Evidence for a fundamental property of steering》

文章目录1 背景2 方法2.1 方向盘修正行为标识2.2 数据2.3 数据拟合3 结果3.1 速率曲线3.2 恒定的转向时间3.3 基本运动元素的叠加3.4 其他实验4 讨论5 总结(个人)1 背景 这篇短文的主要目的是去阐述“转方向盘”这一行为的基本性质:方向盘修正…

人大金仓数据库索引的应用与日常运维

索引的应用 一、常见索引及适应场景 BTREE索引 是KES默认索引,采用B树实现。 适用场景 范围查询和优化排序操作。 不支持特别长的字段。 HASH索引 先对索引列计算一个散列值(类似md5、sha1、crc32),然后对这个散列值以顺序…

SpringBoot+Vue实现师生健康信息管理系统

文末获取源码 开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7/8.0 数据库工具:Navicat11 开发软件:eclipse/myeclipse/idea Maven包:Maven3.3.9 浏…

第二章-线程(3)

线程一、线程的定义二、线程的实现一、线程的定义 线程: 线程是进程中的一个实体,是系统独立调度和分派的基本单位。 进程是资源的拥有者,线程是系统独立调度和分配的基本单位。 进程与线程的比较: 调度:线程调度快…

Python 的Tkinter包系列之七:好例子补充2

Python 的Tkinter包系列之七:好例子补充2 英汉字典(使用文本文件记录英语单词和解释)、简单的通信录(使用SQLite数据库记录人员信息) 一、tkinter编写英汉字典 先看效果图: 词典文件是一个文本文件&…

python自动发送邮件实现

目录1 前言2 准备工作2.1 电子邮件的基础知识。2.2 python邮件库2.3 邮箱设置3 python实现邮件自动发送3.1 SMTP()和send()方法介绍3.2 python实现实例参考信息1 前言 python功能强大,可以实现我们日常办公的很多任务。诸如批量处理word,excel,pdf等等文件&#xf…

事务基础知识与执行计划

事务基础知识 数据库事务的概念 数据库事务是什么? 事务是一组原子性的SQL操作。事务由事务开始与事务结束之间执行的全部数据库操作组成。A(原子性)、(C一致性)、I(隔离性)、D(持久…

1625_MIT 6.828 stabs文档信息整理_下

全部学习汇总: GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 继续之前的学习笔记,整理一下最近看过的一点stabs资料。 这一页中有一半的信息是Fortran专用的,直接跳过。参数的符号修饰符是p&#xff0c…

【webpack】webpack 中的插件安装与使用

一、webpack 插件的作用 通过安装和配置第三方的插件,可以拓展 webpack 的能力,从而让 webpack 用起来更方便。最常用的 的webpack 插件有如下两个: 1.webpack-dev-server(实时打包构建) 类似于 node.js 阶段用到的 no…

为什么说网络安全是风口行业?

前言 “没有网络安全就没有国家安全”。当前,网络安全已被提升到国家战略的高度,成为影响国家安全、社会稳定至关重要的因素之一。 网络安全行业特点 1、就业薪资非常高,涨薪快 2021年猎聘网发布网络安全行业就业薪资行业最高人均33.77万&…

Django框架之模型

模型 当前项目的开发, 都是数据驱动的。 以下为书籍信息管理的数据关系:书籍和人物是 :一对多关系 要先分析出项目中所需要的数据, 然后设计数据库表. 书籍信息表 字段名字段类型字段说明idAutoField主键nameCharField书名 idname1西游记2三国演义…

SpringMVC中遇到的错误

SpringMVC中遇到的错误1.web.xml中配置SpringMVC核心类: DispatcherServlet 报错解决方案:添加Tomcat包2. not declaration can be found for element--------‘mvc:annotation-driven‘通配符的匹配很全面, 但无法找到元素 mvc:annotation-driven 的声明解决方案&a…

雅思经验(9)之小作文常用词汇总结

写作:关于趋势的上升和下降在小作文中,真的是非常常见的,所以还是要积累一下。下面给出了很多词,但是在雅思写作中并不是词越丰富,分数就越高的。雅思写作强调的是准确性:在合适的地方用合适的词和句法。不…

Python+Go实践(电商架构三)

文章目录服务发现集成consul负载均衡负载均衡算法实现配置中心nacos服务发现 我们之前的架构是通过ipport来调用的python的API,这样做的弊端是 如果新加一个服务,就要到某个服务改web(go)层的调用代码,配置IP/Port并发…

8年软件测试工程师经验感悟

不知不觉在软件测试行业,野蛮生长了8年之久。这一路上拥有了非常多的感受。有迷茫,有踩过坑,有付出有收获, 有坚持! 我一直都在软件测试行业奋战, 毕业时一起入职的好友已经公司内部转岗,去选择…

理论七:为何说要多用组合少用继承?如何决定该用组合还是继承?

在面向对象编程中、有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。为什么不推荐使用继承?组合相比继承有哪些优势?如何判断该用组合还是继承?今天,我们就围绕着这三个问题,来…

Windows中安装Docker Desktop 4.16.3(当前最新版本)

前言 docker是一个用Go语言实现的开源项目,它可以很方便的创建和使用容器,docker将程序以及程序所有的依赖都打包到docker container,这样程序可以在任何环境都会有相同的表现,这里程序运行的依赖也就是容器类似集装箱&#xff0…

用于超大图像的训练策略:Patch Gradient Descent

前言 本文旨在计算和内存限制的情况下,解决在大规模图像上训练现有CNN 架构的问题。提出PatchGD,它基于这样的假设:与其一次对整个图像执行基于梯度的更新,不如一次只对图像的一小部分执行模型更新,确保其中的大部分是…

scscanner:一款功能强大的大规模状态码扫描工具

关于scscanner scscanner是一款功能强大的大规模状态码扫描工具,该工具可以帮助广大研究人员从一个URL列表文件中批量读取目标网站的状态码响应信息。除此之外,该工具还可以过滤出指定的状态码,并将结果存储到一个文件中以供后续深入分析使用…

Idea软件——Debug使用方法

Idea软件——Debug 1 Debug概述 Debug:是供程序员使用的程序测试工具,它可以用于查看程序和执行流程,也可以用于追踪程序执行过程来调试程序 1.2 Debug操作流程 Debug调试:又叫断点调试,断点其实是一个标记,告诉从何看…