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.12
中bootsect.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) |
b2 | 0:全局描述符表 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
其中:
ecx
是计数器, 是重复(rep)前缀指令和loop指令的内定计数器,表示控制循环次数- cld相对应的指令是std,二者均是用来操作方向标志位DF(Direction Flag)。cld使DF 复位,即是让DF=0,std使DF置位,即DF=1.这两个指令用于串操作指令中。通过执行cld或std指令可以控制方向标志DF,决定内存地址是增大(DF=0,向高地址增加)还是减小(DF=1,向地地址减小)
- rep 表示当 ecx>0 时,循环继续;反之停止
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 项。
我们简单举个例子:
- 这里的
$pg0+7
其实就表示0x00001007
,是页目录表中的第 1 项,我们按线性地址转换为对应的0b0000000000 0000000001 000000000111
- 按照页目录和页表的结构,我们知晓第 1 个页表所在的地址 =
0000000001
=0x1000
; - 第 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个作用:
- 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
- 将之前压入栈中的
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》
作者:小牛呼噜噜 ,首发于公众号小牛呼噜噜,系列文章还有:
- 聊聊x86计算机启动发生的事?
- Linux0.12内核源码解读(2)-Bootsect.S
- Linux0.12内核源码解读(3)-Setup.S
- 图解CPU的实模式与保护模式
- Linux0.12内核源码解读(5)-head.s
- Linux0.12内核源码解读(6)-main.c
- Linux0.12内核源码解读(7)-陷阱门初始化
- 图解计算机中断
- Linux0.12内核源码解读(9)-blk_dev_init和chr_dev_init
- 什么是系统调用机制?结合Linux0.12源码图解
- tty是什么?聊聊linux0.12中tty与time的初始化
- linux0.12内核源码解读(12)-任务调度初始化sched_init
相关文章:
Linux0.12内核源码解读(5)-head.s
大家好,我是呼噜噜,好久没有更新old linux了,本文接着上一篇文章图解CPU的实模式与保护模式,继续向着操作系统内核的世界前进,一起来看看heads.s as86 与GNU as 首先我们得了解一个事实,在Linux0.12内核源…...
刷代码随想录有感(119):动态规划——打家劫舍III(树形dp)
题干: 代码: class Solution { public:vector<int>dp(TreeNode* cur){if(cur NULL)return vector<int>{0, 0};vector<int> left dp(cur -> left);vector<int> right dp(cur -> right);//偷int val1 cur -> val l…...
vivado CARRY_REMAP、CASCADE_HEIGHT
CARRY_REMAP opt_design-carry_remap选项可用于将单个carry*单元重新映射到LUT中 提高了布线的设计效果。使用-carry_remap选项时,仅 将单级进位链转换为LUT。CARRY_REMAP属性允许您 指定在优化过程中要转换的长度较大的进位链。 您可以使用控制任意长度的单个进位链…...
Ubuntu磁盘分区和挂载 虚拟机扩容 逻辑卷的创建和扩容保姆及教程
目录 1、VMware虚拟机Ubuntu20.04系统磁盘扩容 2、Linux的磁盘分区和挂载 3、创建逻辑卷和逻辑卷的扩容 1、VMware虚拟机Ubuntu20.04系统磁盘扩容 通过下图可以看出我们的根磁盘一共有20G的大小,现在我们把它扩容为30G 注:如果你的虚拟机有快照是无…...
【附精彩文章合辑】哈佛辍学小哥的创业经历【挑战英伟达!00 后哈佛辍学小哥研发史上最快 AI 芯片,比 H100 快 20 倍!】
前情提要 https://blog.csdn.net/weixin_42661676/article/details/140020491 哈佛辍学小哥的创业经历 一、背景与起步 这位哈佛辍学小哥,名为Chris Zhu,是一位华裔学生,他在2020年进入哈佛大学,攻读数学学士学位和计算机科学硕…...
Oracle CPU使用率过高问题处理
1.下载Process Explorer 2.打开Process Explorer,查看CPU使用情况最高的进程 3.双击该进程,查看详情 \ 4. 获取cpu使用最好的线程tid 5. 查询sql_id select sql_id from v$session where paddr in( select addr from v$process where spid in(1…...
pyqt的QWidgetList如何多选?如何按下Ctrl多选?
通过设置setSelectionMode(QAbstractItemView.MultiSelection),可以实现QWidgetList的多选。 但是上述结果不太符合我们需求。设置多选模式后,只需鼠标点击就可以选择多个条目。 我希望按下Ctrl键时才进行多选,仅鼠标单击的话,只进…...
【电路笔记】-MOSFET放大器
MOSFET放大器 文章目录 MOSFET放大器1、概述2、电路图3、电气特性3.1 ** I D = F ( V G S ) I_D=F(V_{GS}) ID=F(VGS)**特性3.2 I D = F ( V D S ) I_D=F(V_{DS}) ID=F(VDS)特性4、MOSFET放大器5、输入和输出电压6、电压增益7、总结1、概述 在前面的文章中,我们已经…...
Ubuntu 20.04安装显卡驱动、CUDA、Pytorch(2024.06最新)
文章目录 一、安装显卡驱动1.1 查看显卡型号1.2 根据显卡型号选择驱动1.3 获取下载链接1.4 查看下载的显卡驱动安装文件1.5 更新软件列表和安装必要软件、依赖1.6 卸载原有驱动1.7 禁用默认驱动1.8 安装lightdm显示管理器1.9 停止显示服务器1.10 在文本界面中,禁用X…...
wpf 附加属性 RegisterAttached 内容属性
// // 摘要: // 选中时展示的元素 public static readonly DependencyProperty CheckedElementProperty DependencyProperty.RegisterAttached("CheckedElement", typeof(object), typeof(StatusSwitchElement), new PropertyMetadata((object)null…...
laravel8框架windows下安装运行
目录 1、安装前如果未安装先安装Composer 2、使用composer安装laravel8 3、使用内置服务器:8000 的命令去访问测试 4、使用本地环境运行phpstudy配置到public目录下 Laravel官网 Laravel 中文网 为 Web 工匠创造的 PHP 框架 安装 | 入门指南 |《Laravel 8 中文文档 8.x…...
如何快速判断IP被墙
IP被墙是指IP部分地区或者运营商无法被正常进行访问的一个情况。 被墙的原因有很多种不一一列举,由于被墙的时间短的为按周按月计算,时间长的则为按年计算,所以一般这种情况下只能选择更换IP。 检查办法: 第一,确认IP…...
vitest-前端单元测试
Vitest是一个轻量级、快速且功能强大的测试框架,特别适用于Vite项目,但也可以与其他前端项目(如使用webpack构建的项目)集成使用。Vitest提供极速的测试体验,并包含一系列用于编写和组织测试用例的API,如de…...
Redis 7.x 系列【9】数据类型之自动排重集合(Set)
有道无术,术尚可求,有术无道,止于术。 本系列Redis 版本 7.2.5 源码地址:https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 前言2. 常用命令2.1 SADD2.2 SCARD2.3 SISMEMBER2.4 SREM2.5 SSCAN2.6 SDIFF2.7 SU…...
【LeetCode】每日一题:反转链表
题解思路 循环的方法需要注意prev应该是None开始,然后到结束的时候prev是tail,递归的思路很难绕过弯来,主要在于很难想清楚为什么可以返回尾节点,需要多做递归题,以及递归过程中,可以不使用尾节点来找当前…...
使用Spring Boot创建自定义Starter
使用Spring Boot创建自定义Starter 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将探讨如何使用Spring Boot创建自定义Starter,来简化项目…...
cmd设置编码为utf8
文章目录 临时设置永久设置(通过注册表) cmd命令乱码,解决方案比较简单。 输入chcp, 如果返回的是936,通常是GBK或CP936。 如果返回的是65001,表示是UTF-8。 临时设置 chcp 65001 # 设置 chcp # 查看 永久设置(通过注册表) 打…...
一次关于k8s的node节点NotReady的故障排查
master现象 分析 kubectl get nodes -A 看了下pod的状态,好多CrashLoopBackOff kubectl get nodes -o wide 定位到那个具体node的IP地址,登录对应的IP去查看为什么会这样 node节点 journalctl -xe -f -u kubelet 查看此节点的 kubelet 服务ÿ…...
Java变量与标识符
一、关键字(Keyboard) 定义:被Java语言赋予了特殊含义,用做专门用途的字符串(或单词) 特点:全部关键字都是小写字母 官方地址: https://docs.oracle.com/javase/tutorial/java/nut…...
AWS无服务器 应用程序开发—第十七章 AWS用户池案例
在AWS Cognito用户池中,用户属性可以根据应用程序的需求进行配置和管理。以下是一般情况下用户属性的一些常见设置: 必须的属性: 用户名(Username):通常用作用户的唯一标识符。 密码(Password…...
java中的枚举
第1部分:引言 枚举在Java中的重要性 枚举在Java中扮演着至关重要的角色,它不仅提高了代码的可读性和可维护性,还增强了类型安全。枚举的使用可以避免使用魔法数字或散列常量,这些在代码中通常难以理解和维护。通过枚举ÿ…...
各种开发语言运行时占用内存情况比较
随着科技的发展,编程语言种类繁多,不同的编程语言在运行时的内存占用情况各不相同。了解这些差异对于开发者选择合适的编程语言尤为重要。本文将讨论几种主流编程语言在运行时的内存占用情况,包括C、C、Java、Python和Go等。 1. C语言 内存…...
【基础知识10】label与input标签
label标签说明 HTML元素表示用户界面中某个元素的说明 将一个和一个元素相关联主要有这些优点: 标签文本不仅与其相应的文本输入元素在视觉上相关联,程序中也是如此。这意味着,当用户聚焦到这个表单输入元素时,屏幕阅读器可以读…...
【SDV让汽车架构“和而不同”】
昔日以“排气管数量”和“发动机动力”为骄傲的荣耀已然成为过往。在这个崭新的时代,特斯拉、理想、蔚来、小鹏、零跑等新兴的汽车制造商纷纷推出了搭载可交互大屏、实现万物互联、软件功能持续更新的新车型,它们被誉为“车轮上的智能手机”。同时&#…...
面试经验分享 | 驻场安全服务工程师面试
所面试的公司:某安全厂商 所在城市:浙江宁波 面试职位:驻场安全服务工程师 面试官的问题: 1、信息收集如何处理子域名爆破的泛解析问题? 泛域名解析是:*.域名解析到同一IP。域名解析是:子域…...
SpringBoot 学习笔记
文章目录 SpringBoot1 SpringBoot 的纯注解配置(了解)1.1 环境搭建1.1.1 jdbc配置1.1.2 mybatis配置1.1.3 transactional配置1.1.4 service配置1.1.5 springmvc配置1.1.6 servlet配置1.1.7 存在的问题 1.2 新注解说明1.2.1 Configuration1.2.2 Component…...
Android 13 为应用创建快捷方式
参考 developer.android.google.cn 创建快捷方式 来自官网的说明: 静态快捷方式 :最适合在用户与应用互动的整个生命周期内使用一致结构链接到内容的应用。由于大多数启动器一次仅显示四个快捷方式,因此静态快捷方式有助于以一致的方式执行…...
PTA—C语言期末复习(选择题)
1. 按照标识符的要求,(A)不能组成标识符。 A.连接符 B.下划线 C.大小写字母 D.数字字符 在大多数编程语言中,标识符通常由字母(包括大写和小写)、数字和下划线组成,但不能以数字开头,…...
基于STM32的智能家用空气净化系统
目录 引言环境准备智能家用空气净化系统基础代码实现:实现智能家用空气净化系统 4.1 数据采集模块4.2 数据处理与分析4.3 控制系统实现4.4 用户界面与数据可视化应用场景:空气净化管理与优化问题解决方案与优化收尾与总结 1. 引言 智能家用空气净化系…...
计算机图形学入门18:阴影映射
1.前言 前面几篇关于光栅化的文章中介绍了如何计算物体表面的光照,但是着色并不会进行阴影的计算,阴影需要单独进行处理,目前最常用的阴影计算技术之一就是Shadow Mapping技术,也就是俗称的阴影映射技术。 2.阴影映射 Shadow Map…...
pt网站怎么做/百度关键词推广方案
即如下: 【想做到点击nav侧边栏,仅替换右边div中的内容,而不是跳转到新的页面,这样的话,其实整个项目中就只有一个完整的页面,其他的页面均只写<body>内的部分即可,或者仅仅写要替换的<div>内的…...
沈阳哪有做网站的/百度资源共享链接分享组
让arch linux更动听(dmix多音流)(转)本文主要是关于在arch Gnome下多音流的实现和开启事件音效(就是像window或KDE下开机关机的背景音乐,和操作时的一些声音效果) 注:声卡支持硬件混音的朋友,不需要瞄小弟的图鸦了&…...
自己电脑做服务器上传网站 需要备案吗/seo如何进行优化
转自:http://www.cnblogs.com/wrmfw/archive/2012/01/21/2328534.html 你发现快要过年了,于是想给你的女朋友买一件毛衣,你打开了www.taobao.com。这时你的浏览器首先查询DNS服务器,将 www.taobao.com转换成ip地址。不过首先你会发…...
深圳住建厅官方网站/web网页制作成品免费
广义表是数据结构中非常关键的一部分,它的学习对于树和二叉树有很大的起承作用。那么,它是怎么实现的呢?广义表的实现应用到了一个很熟悉的算法——递归。来看看它的代码吧!#pragma once #include<iostream> #include<ca…...
牙科医院网站开发/免费发帖推广的平台
一个 nautilus 插件,用于在任意目录中打开终端 nautilus-open-terminal转载于:https://www.cnblogs.com/dirt2/p/5339005.html...
公司网站建设需求书/亚马逊排名seo
在JMeter BeanShell 前置处理器中,您可以使用下面的代码来定义长度为2的数组: int[] myArray new int[2]; myArray[0] 1; myArray[1] 2;该代码创建了一个名为"myArray"的整型数组,并初始化为具有两个元素,分别为1和2。…...