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寄存器赋值0x10
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss _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 #设置IDT


setup_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的偏移给edi
	mov $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 !!!
	ret

idt_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 %eax
	pushl %ecx
	pushl %edx
	push %ds ## 注意!!ds,es,fs,gs 等虽然是 16 位的寄存器,但入栈后仍然会以 32 位的形式入栈,也即需要占用 4 个字节的堆栈空间
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg # 把调用 printk 函数的参数指针(地址)入栈
	call _printk
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret # 中断返回(把中断调用时压入栈的 CPU 标志寄存器(32 位)值也弹出)

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

设置GDT

我们来看下其相关源码:

call setup_gdt #设置GDT

setup_gdt:
	lgdt gdt_descr # 加载全局描述符表寄存器(内容已设置好)
	ret

gdt_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 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

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

检查A20是否打开

xorl %eax,%eax  #清零,xorl只需要2个字节,而是用movl实现清零需要5个字节!
1:	incl %eax		#  检查A20是否开启
	movl %eax,0x000000	# 如果不是,则永远循环
	cmpl %eax,0x100000
	je 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,ET

	orl $2,%eax		# set MP
	movl %eax,%cr0
	call check_x87
	jmp after_page_tables

check_x87:
	fninit      # 向协处理器发出初始化命令
	fstsw %ax   # 取协处理器状态字到 ax 寄存器中
	cmpb $0,%al # 初始化后状态字应该为 0,否则说明协处理器不存在
	je 1f			/* no coprocessor: have to set bits */
	movl %cr0,%eax   # 如果存在则向前跳转到标号 1 处,否则改写 cr0
	xorl $6,%eax		/* reset MP, set EM */
	movl %eax,%cr0
	ret
.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 $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $_main

	jmp 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,%eax
	orl $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 $0
	pushl $0
	pushl $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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

20240628 每日AI必读资讯

📚 Hugging Face 推出新版开源大模型排行榜,中国模型 Qwen-72B 夺冠 - 阿里Qwen-2-72B指令微调版本问鼎全球开源大模型排行榜榜首 - Llama-3-70B 微调版本排名第二,而 Mixtral-8x22B 微调版本位居第四。 - 另外,微软的 Phi-3-M…

卸载vmware时2503,2502报错的解决办法

1.背景 windows 卸载vmware时,显示2503报错,无法完全卸载 2. 解决方案 2.1 参考安装报错2502,2503的处理方式 文献:https://blog.csdn.net/zhangvalue/article/details/80309828 2.1 步骤: 2.1.1 cmd 管理员打开…

字节码编程ASM之插桩方法执行耗时

写在前面 本文看下如何对已有类进行插装。以最经典的方法执行耗时作为例子。 1:编码 假定有如下的代码: public class MyMethod {public String queryUserInfo(String uid) {System.out.println("xxxx");System.out.println("xxxx1&q…

可的哥Codigger项目体检是衡量代码质量标准

在飞速发展的现代商业世界中,项目能否成功的核心要素是项目质量,也就是其健康状态。为了确保项目顺利进行并达到预期目标,项目体检工具(Health Check),简称“项目体检”,变得尤为重要。可的哥&a…

一分钟学习数据安全—自主管理身份SSI分布式标识DID介绍

SSI标准化的两大支柱,一个是VC,之前简单介绍过,另一个就是DID。基本层次上,DID就是一种新型的全局唯一标识符,跟浏览器的URL没有什么不同。深层次上,DID是互联网分布式数字身份和PKI新层级的原子构件。 一…

猫咪主食冻干哪个牌子好?希喂、SC、鲜朗人气养猫好物强烈推荐

目前主食冻干市场产品良莠不齐,一些主食冻干品牌一味追求堆砌营养值和利润,实际毫不关心猫咪食品健康,不仅存在肉粉冒充鲜肉、临期改日期卖等问题,甚至出现并为送检第三方、细菌超标等情况,严重的甚至危及猫咪生命&…

从单点到全景:视频汇聚/安防监控EasyCVR全景视频监控技术的演进之路

在当今日新月异的科技浪潮中,安防监控领域的技术发展日新月异,全景摄像机便是这一领域的杰出代表。它以其独特的360度无死角监控能力,为各行各业提供了前所未有的安全保障,成为现代安防体系中的重要组成部分。 一、全景摄像机的技…

ISO 50001能源管理体系:激活绿色动能和共塑可持续发展

在当今全球化加速和工业化水平不断提高的背景下,能源消费呈现出前所未有的增长趋势。然而,能源资源的有限性、能源价格的波动以及能源消费对环境造成的影响,尤其是温室气体排放导致的全球气候变化问题,已经成为全球关注的焦点。为…

2024 6.17~6.23 周报

一、上周工作 吴恩达的机器学习、实验-回顾之前密集连接部分 二、本周计划 继续机器学习,同时思考实验如何修改,开始整理代码 三、完成情况 3.1 多类特征、多元线性回归的梯度下降、特征缩放、逻辑回归 多类特征: 多元线性回归的梯度下…

远程工具的使用

远程连接工具的作用,通过远程连接到服务器上,方便操作! 1.常见的远程连接工具 XShell:这是一款Windows平台下的SSH客户端软件,支持SSH1、SSH2、SFTP、TELNET、RLOGIN等多种协议,功能丰富,包…

frida的安装使用以及解决抓包app时遇到的证书校验

frida的安装和使用 这里使用夜神模拟器来演示frida的使用,因为真机开启frida-server服务时需要root权限,模拟器自带root 下载夜神模拟器并启动 夜神官网 打开power shell, adb连接模拟器,查看模拟器的系统型号 adb connect 127.0.0.1:6200…

解锁高效运维新纪元:网络基础设施数字孪生管理工具

随着信息技术的飞速发展,网络基础设施的运维管理变得日益复杂。北京耐威迪科技股份有限公司凭借其创新技术,推出了nVisual网络基础设施数字孪生管理工具,这一革命性的解决方案不仅提升了运维效率,更在成本节约和项目进度上实现了突…

【Redis】Set 集合常用命令以及使用场景

集合(Set)类型的值是字符串的无序集合,并且每个值都是唯一的。本文将介绍 Redis Set 的常用命令包含示例、Set的内部编码以及使用场景。 集合类型也是保存多个字符串类型的元素的,但和列表类型不同的是,集合中 1)元素…

2024最新总结:1500页金三银四面试宝典 记录35轮大厂面试(都是面试重点)

学习是你这个职业一辈子的事 手里有个 1 2 3,不要想着去怼别人的 4 5 6,因为还有你不知道的 7 8 9。保持空瓶心态从 0 开始才能学到 10 全。 毕竟也是跳槽高峰期,我还是为大家准备了这份1500页金三银四宝典,记录的都是真实大厂面…

VS2019安装插件image watch

image watch的作用: (1)放大、缩小图像; (2)将图像保存到指定的目录; (3)显示图像大小、通道数; (4)拖拽图像; &…

jenkins nginx自动化部署 php项目

在当今快速发展的IT领域,自动化部署已成为提高工作效率和减少错误的关键。Jenkins作为持续集成/持续部署(CI/CD)的佼佼者,结合Docker容器技术和PHP编程语言,以及Ansible自动化工具,可以实现高效、可靠的自动…

AppFlow无代码轻松搭建模型Agent

随着大语言模型发展至今,如何深度开发和使用模型也有了各种各样的答案,在这些答案当中,Agent无疑是一个热点回答。 通过模型也各种插件的组合,可以让你的模型应用具备各种能力,例如,通过天气查询插件机票查…

使用 SwiftUI 为 macOS 创建类似于 App Store Connect 的选择器

文章目录 前言创建选择器组件使用选择器组件总结前言 最近,我一直在为我的应用开发一个全新的界面,它可以让你查看 TestFlight 上所有可用的构建,并允许你将它们添加到测试群组中。 作为这项工作的一部分,我需要创建一个组件,允许用户从特定构建中添加和删除测试群组。我…

MySQL周内训参照3、简单查询与多表联合复杂查询

基础查询 1、查询用户信息,仅显示用户的姓名与手机号,用中文显示列名。中文显示姓名列与手机号列 SELECT user_id AS 编号, phone AS 电话 FROM user; 2. 根据订购表进行模糊查询,模糊查询需要可以走索引,需要给出explain语句。…

什么是yum源?如何对其进行配置?

哈喽,大家好呀!这里是码农后端。今天来聊一聊Linux下的yum源及其配置相关的内容。简单来说,yum源就相当于一个管理软件的工具,可以想象成一个很大的仓库,里面存放着各种我们所需要的软件包及其依赖。 一、Linux下软件包…