Linux0.12内核源码解读(5)-head.s

大家好,我是呼噜噜,好久没有更新old linux了,本文接着上一篇文章图解CPU的实模式与保护模式,继续向着操作系统内核的世界前进,一起来看看heads.s

as86 与GNU as

首先我们得了解一个事实,在Linux0.12内核源码中,其实是使用了2套汇编器Assembler的,一种是Intel8086汇编编译器as86和配套的链接器ld86,并一种就是GNU as(gas),使用 GNU ld 链接器来链接产生的目标文件。

为什么使用了2套汇编器?

我们知道Linux0.12bootsect.s和setup.s实模式下运行的16位代码程序,而那个时候的GNU as 汇编编译器无法支持16位实模式代码程序编译,所以Linus不得不使用as86和ld86,其语法近似Intel语法

而从head.s开始的,内核完全都是在保护模式下运行了,操作系统system模块中其余所有汇编语言程序(包括 C 语言产生的汇编程序)都是使用GNU as 汇编编译器,使用的是AT&T语法。直到Linux内核2.4.x后,bootsect.s和head.s程序才完全使用统一的GNU as 来编写

2种语法虽然是有所区别,但其实都是类似的,需要注意的最基本的区别是,AT&T语法中,mov赋值的方向是从左到右

在Linux0.12内核源码解读(3)-Setup.S中,最后我们说到CPU 进入了 32 位保护模式,跳到了内存零地址处开始执行代码。先来回顾一下执行完setup.S时的内存分布情况:

作者:小牛呼噜噜


此时从内存零地址处存放的system模块,其首部是head.s代码,即head.s代码从地址0处开始存放,因此setup结束后执行的就是head.s文件

head.s主要是进入进行保护模式之后的初始化,主要初始化些什么呢?呼噜噜,画了个流程图,建议大家跟着下面流程图,阅读以下全文

如果有人对本文中操作系统一系列初始化操作,感到疑惑,比如为什么要设置的话等之类的问题,建议先看笔者前一篇文章图解CPU的实模式与保护模式

设置段寄存器和系统堆栈

_pg_dir: # 页目录将会存放在这里
startup_32:movl $0x10,%eax # 32位ax寄存器赋值0x10mov %ax,%dsmov %ax,%esmov %ax,%fsmov %ax,%gslss _stack_start,%esp #设置栈(系统栈)

我们可以看到上面这段源代码中_pg_dir,这个很重要,和分页机制有关,主要是标识内核分页机制完成后的内核起始地址(零地址),页目录将会存放在这里,这个我们下文再讲。

movl $0x10,%eax,将32位ax寄存器赋值0x10,MOV类指令是最简单的数据传送指令,这类指令把数据从源位置复制到目的位置,需要声明要传送的数据元素的长度,一般有以下几种:

指令描述位数
movb传送字节8位
movw传送字16位
movl传送双字32位
movq传送四字64位

对于 GNU 汇编,每个直接操作数要以$开始,否则表示地址。每个寄存器名都要以%开头,eax 表示是 32 位的 ax 寄存器。

如果面试官提问head.s中0x10这个地址具体是指向哪呢?

这个是虽然简单,但很有迷惑性的,首先我们得知道当操作系统执行head.s的时候,已经进入了保护模式,此时段寄存器不再表示段的基地址,而是表示段选择符(也叫段选择子)

段选择符描述
b1-b0请求特权级(RPL)
b20:全局描述符表 1:局部描述符表
b15-b3描述符表项的索引, 指出选择第几项描述符(从0开始)

所以我们需要先0x10写成16位二进制形式(高位补零)0b0000 0000 0001 0000,所以对应的段选择符:请求特权级为 0(RPL=00)、所指向的描述符存放在GDT(T1=0)、所指向的描述符索引为2(DI=0000 000000010),也就是指向GDT全局段描述符表第3项(从0开始)

接着分别给 ds、es、fs、gs 这几个段寄存器赋值为0x10,让这些寄存器都指向GDT的第3项

lss _stack_start,%esp主要作用是设置系统栈,汇编指令lss会分别给一个段寄存器和一个16位通用寄存器赋值,那么也就是说将操作数_stack_start的值传送给指定ss:esp,其中ss就是堆栈寄存器,存放堆栈段的段基址(实模式),保护模式下存放的就是段选择符,只能存放16位的数据,esp是指向栈顶的通用寄存器,能够存放32位的数据

stack_start是一个标号,它定义在kernel/sched.c文件中:

#定义用户堆栈, PAGE_SIZE=4096,所以user_stack长度为1024
long user_stack [PAGE_SIZE=4096>>2 ] ;struct {long * a;short b;} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

我们可以发现这是一个结构体,将stack_start的值传给ss:esp,lss指令会把stack_start指向的内存地址的前四字节(32位)装入ESP寄存器,后两字节(16位)装入SS段寄存器,即ss=0x10,esp=& user_stack [1024]

设置IDT

call setup_idt #设置IDTsetup_idt:lea ignore_int,%edx  #将 ignore_int 的有效地址(偏移值)值 赋值给 edx 寄存器movl $0x00080000,%eax # 将段选择符 0x0008 置入 eax 的高 16 位中movw %dx,%ax		/* selector = 0x0008 = cs */ # 偏移值的低 16 位置入 eax 的低 16 位中。此时 eax 含有门描述符低 4 字节的值movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */ #此时 edx 含有门描述符高 4 字节的值lea _idt,%edi # _idt 是中断描述符表的地址, 取idt的偏移给edimov $256,%ecx #循环256次
rp_sidt:movl %eax,(%edi) # 将哑中断门描述符存入表中movl %edx,4(%edi) # eax 内容放到 edi+4 所指内存位置处。addl $8,%edi    # edi 指向表中下一项dec %ecx # 循环减1 jne rp_sidt  jne 表示zf=0跳转lidt idt_descr  # 加载IDTR !!!retidt_descr:.word 256*8-1		# idt contains 256 entries ,共 256 项,是CPU寄存器中的值.long _idt
.align 2
.word 0_idt:	.fill 256,8,0		# idt is uninitialized,这个是在内存中的

IDT,Interrupt Descriptor Table,即中断描述符表,记录着0~255的中断号和调用函数之间的关系,与中段向量表有些相似,但要包含更多的信息。

不知道大家还记不记得,在setup.S中临时将IDT临时设置为一个空表,自此int n 不再是DOS中断了,而是去IDT表中找到中断函数的地址,再执行

上面这段代码实现了256 个中断描述符的设置,各个中断描述符表项都指向一个ignore_int的函数地址,其中ignore_int是一个只报错误的哑中断子程序,内核在随后的初始化过程中,会替换覆盖那些真正实用的中断描述符项

我们查看ignore_int,会发现它就是去打印一串字符Unknown interrupt,提示报错

int_msg:.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:pushl %eaxpushl %ecxpushl %edxpush %ds ## 注意!!ds,es,fs,gs 等虽然是 16 位的寄存器,但入栈后仍然会以 32 位的形式入栈,也即需要占用 4 个字节的堆栈空间push %espush %fsmovl $0x10,%eaxmov %ax,%dsmov %ax,%esmov %ax,%fspushl $int_msg # 把调用 printk 函数的参数指针(地址)入栈call _printkpopl %eaxpop %fspop %espop %dspopl %edxpopl %ecxpopl %eaxiret # 中断返回(把中断调用时压入栈的 CPU 标志寄存器(32 位)值也弹出)

中断对操作系统来说非常重要,可以跟硬件(例如键盘鼠标显卡等)产生交互,没有中断操作系统就缺胳膊少腿,当中断发生时,CPU获取到中断向量后,通过IDTR的值,去查找IDT中断描述符表,得到相应的中断描述符,再根据中断描述符记录的信息来作权限判断,运行级别转换,最终调用相应的中断处理程序

设置GDT

我们来看下其相关源码:

call setup_gdt #设置GDTsetup_gdt:lgdt gdt_descr # 加载全局描述符表寄存器(内容已设置好)retgdt_descr:.word 256*8-1		# so does gdt (not that that's any.long _gdt		# magic number, but it works for me :^).align 3_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,其实这里和我们在Setup.S设置的GDT是一样的,笔者这里再贴一下之前的代码,比较一下发现是初始化出来的GDT是基本是一模一样的,除了此时段限长不是原来的8MB,而是现在的16MB

gdt:              ! 描述符表由多个8字节长的描述符项组成。这里给出了 3 个描述符项。.word	0,0,0,0		! dummy 第1个为空描述符,无用,但必须存在.word	0x07FF		! 段界限为 8M,limit=2047 (2048*4096=8Mb) 第2个为空描述符.word	0x0000		! 段基址为 0.word	0x9A00		! code read/exec P=1, DPL=00, S=1, 代码段,只读,可执行.word	0x00C0		! granularity=4096, 386 .word	0x07FF		! 段界限为 8M - limit=2047 (2048*4096=8Mb) 第3个为空描述符.word	0x0000		! 段基址为 0.word	0x9200		! P=1, DPL=00, S=1, 数据段,可读可写.word	0x00C0		! granularity=4096, 386

这里主要是为了防止GDT这块内存区域被其他程序覆盖使用,head废除Setup.S设置的GDT,并在内存中重新创建一个全新的全局描述符表

重复设置段寄存器与系统堆栈

	movl $0x10,%eax		# reload all the segment registersmov %ax,%ds		# after changing gdt. CS was alreadymov %ax,%es		# reloaded in 'setup_gdt'mov %ax,%fsmov %ax,%gslss _stack_start,%esp

这里重复设置段寄存器与系统堆栈,也是为了安全起见,因为它们所指向的原描述符所指向的段的段限长为 8MB,而刚刚在setup_gdt** 修改了 GDT,段限长已经变为 16MB**,所以当访问 8MB 以上的地址空间时,有可能会产生段限长超限报警。为了防止这类可能发生的情况,在这里重载刷新所有的段寄存器

检查A20是否打开

xorl %eax,%eax  #清零,xorl只需要2个字节,而是用movl实现清零需要5个字节!
1:	incl %eax		#  检查A20是否开启movl %eax,0x000000	# 如果不是,则永远循环cmpl %eax,0x100000je 1b               # '1b'表示向后(backward)跳转到标号 1 去

引入A20是为解决80286的一个bug而引入的,什么bug?请移步看前文Linux0.12内核源码解读(3)-Setup.S

在A20关闭的情况下,系统仍然使用8086/8088的方式,计算机处于20位的寻址模式,访问超过0xFFFFF=2^20=1MB内存时,会自动回卷,比如0x100000会回卷到0x000000;当在A20打开的情况下,才会突破地址信号线20位的宽度,变成32位可用,实现最大寻址空间4GB

所以这部分代码,是通过在内存0x000000处写入任意数据,并和0x100000处比较是否一致,来检查A20是否打开。如果一直相同的话,说明内存回卷, A20没有打开,然后就会一直比较下去,即死循环。

检查x87协处理器是否存在

为了弥补 x86 系列在进行浮点计算时的不足,Intel 于 1980 年推出了 x87 系列数学协处理器,那时是一个外置的、可选的芯片。1989 年,Intel 发布了 486 处理器。自从 486 开始,以后的 CPU 一般都内置了协处理器。这样,对于 486 以前的计算机而言,操作系统检验 x87 协处理器是否存在就非常有必要了

/** 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.*注意! 在下面这段程序中,486 应该将位 16 置位,以检查在超级用户模式下的写保护,* 此后 "verify_area()" 调用就不需要了。486 的用户通常也会想将 NE(#5)置位,以便* 对数学协处理器的出错使用 int 16*/movl %cr0,%eax		# 校验数学芯片andl $0x80000011,%eax	# Save PG,PE,ETorl $2,%eax		# set MPmovl %eax,%cr0call check_x87jmp after_page_tablescheck_x87:fninit      # 向协处理器发出初始化命令fstsw %ax   # 取协处理器状态字到 ax 寄存器中cmpb $0,%al # 初始化后状态字应该为 0,否则说明协处理器不存在je 1f			/* no coprocessor: have to set bits */movl %cr0,%eax   # 如果存在则向前跳转到标号 1 处,否则改写 cr0xorl $6,%eax		/* reset MP, set EM */movl %eax,%cr0ret
.align 2      # align 是一汇编指示符。其含义是指存储边界对齐调整,"2"表示把随后的代码或数据的偏移位置# 调整到地址值最后 2 比特位为零的位置(2^2),即按 4 字节方式对齐内存地址
1:	.byte 0xDB,0xE4		/* fsetpm for 287, ignored by 387 */ret

这部分源码主要是,用于检查数学协处理器芯片是否存在。方法是修改控制寄存器 CR0,在假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在, 需要设置 CR0 中的协处理器仿真位 EM(位 2),并复位协处理器存在标志 MP(位 1),这部分简单了解一下即可

构建分页管理机制

检查完数学协处理器芯片是否存在,紧接着就执行jmp after_page_tables跳转after_page_tables这个标号处:

after_page_tables:# 先将main函数参数,L6标号和main函数入口地址压栈pushl $0		# These are the parameters to main :-)pushl $0pushl $0pushl $L6		# return address for main, if it decides to.pushl $_mainjmp setup_paging 
L6:jmp L6			# main应该永远不会回到这里,但以防万一,我们需要知道发生了什么

先将main函数参数,L6标号和main函数入口地址压入栈中,等待被使用,我们这里先卖个关子,讲完分页再讲解

jmp setup_paging 跳到分页设置,想要理解这部分,你得先了解什么是段页机制,详情见图解CPU的实模式与保护模式


记住这张图的分页机制,理解线性地址前10位,中间10位,后12位分别代表什么,CR3指向哪边,分页机制的原理,我们接着阅读以下部分

内存页清零

setup_paging:movl $1024*5,%ecx		xorl %eax,%eax      # 清零xorl %edi,%edi			# 清零,并让页目录从 0x000 地址开始cld;rep;stosl       # eax 内容存到 es:edi 所指内存位置处,且 edi 增 4

其中:

  1. ecx是计数器, 是重复(rep)前缀指令和loop指令的内定计数器,表示控制循环次数
  2. cld相对应的指令是std,二者均是用来操作方向标志位DF(Direction Flag)。cld使DF 复位,即是让DF=0,std使DF置位,即DF=1.这两个指令用于串操作指令中。通过执行cld或std指令可以控制方向标志DF,决定内存地址是增大(DF=0,向高地址增加)还是减小(DF=1,向地地址减小)
  3. rep 表示当 ecx>0 时,循环继续;反之停止
  4. stosl指令相当于将eax中的值保存到es:edi指向的地址中,若设置了EFLAGS中的方向位置位(即在STOSL指令前使用STD指令)则EDI自减4,否则(使用CLD指令)EDI自增4

这一小段代码连起来就是按4字节的速度循环清空内存,每次循环清空的内存范围** **1024*4=4096字节,恰好是一个页,也就是最终清空5页内存(1 页目录 + 4 页页表)

设置页目录表、页表

因为我们(内核)共有 4 个页表,所以只需设置 4 项。

  # 分别设置4个页表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		/*  --------- " " --------- */

可能就有人会问了,为啥就只有 4 个页表?不是可以设置1024项嘛?

Linx0.12 当时规定最大寻址空间0xFFFFFF,也就是16M,而1个页目录表或者一个页表最多有1024 个项,页的大小固定为4KB,4(页表数)* 1024* 4KB= 16MB,所以只需前4个页表就能够支持16M寻址

咳咳,还记得我们本文一开始讲的_pg_dir,表示页目录表将会存放在这里(零地址处),紧挨着的其实还有4个页表

.org 0x1000 # .ORG伪指令用来表示起始的偏移地址,紧接着ORG的数值就是偏移地址的起始值
pg0:.org 0x2000
pg1:.org 0x3000
pg2:.org 0x4000
pg3:.org 0x5000

页目录项的结构与页表中项的结构一样,4 个字节为 1 项。
我们简单举个例子:

  1. 这里的$pg0+7其实就表示0x00001007,是页目录表中的第 1 项,我们按线性地址转换为对应的0b0000000000 0000000001 000000000111
  2. 按照页目录和页表的结构,我们知晓第 1 个页表所在的地址 =0000000001= 0x1000
  3. 第 1 个页表的属性标志 =000000000111=0x07,在二进制下,根据这3个1分别表示:页存在P=1、用户可读写RW=1、特权为用户态US=1,表示该页存在、用户可读写

原本页表0到页表3处的代码(也就是head.s17行到114行之间所有执行过的代码),全部清空,此时页目录表和页表在内存的分布情况:

+
| ...
+——————— 0x5000
| 页表3
+——————— 0x4000
| 页表2
+——————— 0x3000
| 页表1
+——————— 0x2000,页的大小4K
| 页表0
+——————— 0x1000
| 页目录表
+——————— 0x0000

接着就是填充4个页表中所有项的内容,下面是从最后一个页表的最后一项开始按倒退顺序填充数据

	movl $pg3+4092,%edi   # edi最后一页表的最后一项movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */std              #方向位置位,edi 值递减(4 字节)。
1:	stosl			/* fill pages backwards - more efficient :-) */subl $0x1000,%eax # 每填写好一项,物理地址值减 0x1000。jge 1b      /*1b 表示向后跳转到标号1处,如果小于 0 则说明全添写好了*/

设置CR3和CR0

接着设置页目录表基址寄存器cr3,指向页目录表。cr3中保存的是页目录表的物理内存地址,然后设置启动使用分页处理(cr0 的 PG 标志),cr0中含有控制处理器操作模式和状态的系统控制标志

xorl %eax,%eax		# 页目录表在 0x0000 处。movl %eax,%cr3		# 设置页目录基址寄存器CR3的值,指向页目录表。页目录表在0x0000处movl %cr0,%eaxorl $0x80000000,%eax  movl %eax,%cr0		/* 设置启动使用分页处理,CR0的PG标志置位 */ret			/* this also flushes prefetch-queue */

需要注意的是,当执行完这行代码movl %eax,%cr0后,标志着操作系统正式开启分页,此时段部件产生的地址就不再被看成物理地址,被称为线性地址,而是要送往页部件进行变换,以得到真正的物理地址。

最后ret指令很重要,它这里有2个作用:

  1. 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
  2. 将之前压入栈中的 main()程序入口地址弹出,并跳转到 init/main.c 程序去运行。

乍眼一看ret指令怎么就和main函数联系到一起了?我们马上详细来聊聊其中的缘由

跳转至main函数

跳转至main函数的准备工作其实在head.s的早就开始了,但最后一步由ret指令执行的

after_page_tables:pushl $0		# These are the parameters to main :-)pushl $0pushl $0pushl $L6		# return address for main, if it decides to.pushl $_main...
setup_paging:...ret

after_page_tables标号处,先将main函数参数,L6标号和main函数入口地址压入中,等待被使用。这些参数比如3个0,后续实际上也没有用到。 L6标号是main函数返回时的跳转地址。

汇编中的参数一般是通过寄存器传递的,而C语言中的参数一般是通过栈来传递

直到setup_paging标号处的ret指令,正好将之前压入栈中的 main()程序入口地址弹出,这个时候CPU会把esp寄存器(始终指向栈顶地址)指向的内存地址处的值,赋值给eip寄存器

eip指令指针寄存器存储着下一条指令的地址,通过CS:EIP联合指向即将执行的下一条指令。对于顺序执行的指令,EIP从前一条指令边界移到下一条指令边界上;对于控制转移指令,例如JMP,JCC, CALL,RET和IRET指令,EIP会向前或先后跳跃数条指令。

一般情况下,程序是不能直接读取或修改EIP寄存器的值,但是可以隐式地通过控制转移指令(JMP,J,CALL和RET),中断,和异常来间接控制EIP。要想读取到EIP寄存器的值,唯一的手段是执行CALL指令,然后从程序栈中读取返回指令指针。这里是通过修改程序栈中返回指令指针的值,然后执行RET指令,间接的加载EIP寄存器

最终CPU跳转到 init/main.c处去运行程序代码。

当执行完ret指令,标志着head.s程序到此就真正结束了!

后续就进入了我们倍感亲切的C程序世界,我们下期再见~~


参考资料:
https://elixir.bootlin.com/linux/0.12/source/boot/head.s
英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A-英特尔®
《Linux内核完全注释5.0》


作者:小牛呼噜噜 ,首发于公众号小牛呼噜噜,系列文章还有:

  1. 聊聊x86计算机启动发生的事?
  2. Linux0.12内核源码解读(2)-Bootsect.S
  3. Linux0.12内核源码解读(3)-Setup.S
  4. 图解CPU的实模式与保护模式
  5. Linux0.12内核源码解读(5)-head.s
  6. Linux0.12内核源码解读(6)-main.c
  7. Linux0.12内核源码解读(7)-陷阱门初始化
  8. 图解计算机中断
  9. Linux0.12内核源码解读(9)-blk_dev_init和chr_dev_init
  10. 什么是系统调用机制?结合Linux0.12源码图解
  11. tty是什么?聊聊linux0.12中tty与time的初始化
  12. linux0.12内核源码解读(12)-任务调度初始化sched_init

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

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

相关文章

2024年6月27日 (周四) 叶子游戏新闻

老板键工具来唤去: 它可以为常用程序自定义快捷键,实现一键唤起、一键隐藏的 Windows 工具,并且支持窗口动态绑定快捷键(无需设置自动实现)。 喜马拉雅下载工具: 字面意思 Steam国产“类8番”游戏《永恒逃脱:暗影城堡》…

【JD-GUI】MacOS 中使用Java反编译工具JD-GUI

希望文章能给到你启发和灵感~ 如果觉得文章对你有帮助的话,点赞 关注 收藏 支持一下博主吧~ 阅读指南 开篇说明概念理解一、基础环境说明1.1 硬件环境1.2 软件环境 二、下载与安装2.1 选择对应版本2.2 解压运行排除异常:2.3 关于…

GuiLite C语言实现版本

简介 本项目是idea4good/GuiLite的C语言实现版本,基于2024-06-20节点的版本(提交ID:e9c4b57)。 本项目仓库:GuiLite_C 需求说明 作为芯片从业人员,国产芯片普遍资源有限(ROM和RAM比较少-都是…

[Vulnhub] wallabysnightmare LFI+RCE+Irssi聊天服务RCE

信息收集 Server IP AddressOpening Ports192.168.8.105TCP:22,80,6667,60080 $ nmap -p- -sC -sV 192.168.8.105 --min-rate 1000 -Pn 基础Shell http://192.168.8.105/?page../../../../../etc/shadow 当再次尝试访问已经关闭 $ nmap -p- -sC -sV 192.168.8.105 --min-rat…

【PTA】7-1 网红点打卡攻略(C/C++)代码实现 反思

解题细节分析: 0.比较图的两种存储方法,通过邻接矩阵存储更便于查找给定两点之间的关系 1.注意理解清楚题义:“访问所有网红点”中所有不是指攻略中所有,而是存在的全部的网红点 代码见下:// 需要注明的是&#xff…

锦江丽笙酒店稳步拓局海内外酒店市场 签约及意向合作20个新项目

(中国上海,2024年6月27日)民族品牌的国际化发展已日趋成为推动经济和文化交流的重要力量。作为民族品牌与国际品牌的融合发展,锦江丽笙酒店顺应市场趋势有序推进旗下品牌矩阵的全面布局;2024年上半年,已达成…

简易深度学习(1)深入分析神经元及多层感知机

一、神经元 单个神经元结构其实可以认为是一个线性回归模型。例如下图中 该神经元输入为三个特征(x1,x2,x3),为了方便理解,大家可以认为每条线上都有一个权重和特征对应(w1,w2&…

从源码到上线:直播带货系统与短视频商城APP开发全流程

很多人问小编,一个完整的直播带货系统和短视频商城APP是如何从源码开发到最终上线的呢?今天,笔者将详细介绍这一全过程。 一、需求分析与规划 1.市场调研与需求分析:首先需要进行市场调研,了解当前市场的需求和竞争情…

入职必备-Git 2种方式拉取代码

【SSH方式】: 1.复制电子邮箱 2.git bash 打开窗口 ssh-keygen -t rsa -C liuchangprimecare.group 3.一路回车,然后查看C:\Users\LiuChang.ssh里面的文件 打开id_rsa.pub文件,复制代码 4.添加到GitLab的公钥输入框 5.然后 git clone gitgitlab.pr…

使用容器配置文件构建任意应用镜像_并将应用镜像推送到公共仓库共享_应用分享与启动---分布式云原生部署架构搭建012

上面我们编写好了应用,并且,安装好了redis 现在我们把应用打包成镜像. 以前是这样做的,不方便,因为需要在服务器上,安装jdk什么的,现在有了 镜像就不用,给服务器安装镜像什么的了 以后所有机器都安装docker以后,就直接运行就可以了 首先看一下,安装java应用,需要 用到openjd…

指纹浏览器是什么?跨境多账号安全如何保证?

随着电子商务的蓬勃发展,越来越多的商家选择开设多店来扩大经营规模。然而多店运营也带来了一系列的挑战,其中之一就是账号安全。 1. 了解反检测浏览器和代理服务器 在我们开始讨论如何有效地使用反检测浏览器之前,我们首先需要了解这两个工…

openlayer 我的标注功能

背景: 通过openlayer库,可以在地图上实现绘制点、线、面。 并把绘制的结果添加到我的标注的弹框。 我的标注功能,包括:我的标注查询结果的数据展示;添加分组;添加我的标注;编辑分组、删除分组&a…

经典神经网络(13)GPT-1、GPT-2原理及nanoGPT源码分析(GPT-2)

经典神经网络(13)GPT-1、GPT-2原理及nanoGPT源码分析(GPT-2) 2022 年 11 月,ChatGPT 成功面世,成为历史上用户增长最快的消费者应用。与 Google、FaceBook等公司不同,OpenAI 从初代模型 GPT-1 开始,始终贯彻只有解码器&#xff0…

【机器学习300问】134、什么是主成分分析(PCA)?

假设你的房间堆满了各种各样的物品,书籍、衣服、玩具等等,它们杂乱无章地散落各处。现在,你想要清理房间,但又不想扔掉任何东西,只是希望让房间看起来更整洁,更容易管理。 你开始思考,能否将物品…

以数治税时代来临,企业如何应对?

全电发票是数字经济时代发票的新形态,顺应了数字经济潮流。现如今,国家正全力推动行业数字化进程,预计,2025年将基本实现发票全领域、全环节、全要素电子化,实现税务执法、服务、监管与大数据智能化应用深度融合、高效…

Spring事务的源码底层实现

文章目录 事务理论执行过程EnableTransactionManagement底层实现 事务 在线流程图 理论执行过程 通过事务管理器创建一个连接对象connection1设置事务隔离级别、是否只读等conn1.autocommit(false)将conn1存入ThreadLocal中Map<DataSource,Connection>执行目标方法、多…

Session会话与请求域的区别

session会话和请求域&#xff08;也称为request域&#xff09;都是用于存储和管理用户特定信息的重要概念&#xff0c;但它们在作用范围和生命周期上有显著的不同。 请求域 (Request Domain) 作用范围&#xff1a;请求域是面向单次请求的。每次HTTP请求都会创建一个新的request…

Java中的程序异常处理介绍

一、异常处理机制 Java提供了更加优秀的解决办法&#xff1a;异常处理机制。 异常处理机制能让程序在异常发生时&#xff0c;按照代码的预先设定的异常处理逻辑&#xff0c;针对性地处理异常&#xff0c;让程序尽最大可能恢复正常并继续执行&#xff0c;且保持代码的清晰。 Ja…

算法刷题日志 —— 数组和位运算

文章目录 [461. 汉明距离](https://leetcode.cn/problems/hamming-distance/submissions/542447020/)[448. 找到所有数组中消失的数字](https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/submissions/)[136. 只出现一次的数字](https://leetcode.cn/pro…

最长回文串

描述&#xff1a; 最长回文串 思路&#xff1a; 统计每个字母出现次数&#xff0c;如果是偶数&#xff0c;ret x;如果是存在奇数的话&#xff0c;就可以放在中间&#xff0c;ret 1. 代码&#xff1a; class Solution { public:int hash[200];int longestPalindrome(str…