当前位置: 首页 > news >正文

windows无盘启动技术开发之传统BIOS(Legacy BIOS)引导程序开发之一

by fanxiushu 2023-03-01 转载或引用请注明原始作者。

这个话题可能有点老,UEFI BIOS 已经大量存在,而Legacy BIOS最终会被取代。

但是也是作为无盘启动技术里不可或缺的,毕竟还有许多老型号的电脑存在,

而且为了兼容性,有些新的电脑主板还保留着Legacy BIOS。

开始之前,我们需要预备一些知识,

首先是对16位程序的开发知识,那是属于DOS时代的基本知识。

新一代程序员对这个应该并不清楚,我了解得也不多。就是很早以前接触过 TurboC,学习时候使用的。

因此也多多少少了解一些概念,但仅此而已。

想想有些感概,仅仅二三十年,从简陋的命令行,到复杂图像界面,到手机,平板,各种终端,影响和深入到各行各业,

从无联网的单机,到复杂的各种各样的网络,从0娱乐,到各种各样的极限娱乐。

简单的说,从无到有,都尽情显示着人类这个智慧生物无穷的创造力。

略微有些遗憾的是,这中间中国的参与度不高,大部分都是被动接受,当然被动的总比不接受的好。

当然有些超纲的,目前也无法解决。

比如无法达到超光速信号通信,更无法达到光速航行,或者超光速航行。

这应该是人类脱离地球,在宇宙间任意翱翔的基础,

而以现在的理论基础,好像几乎是不可行的,但愿有天能推翻现有理论,找到超光速航行的办法。

但也有可能在还没找到解决办法的时候,就会迎来地球生物的灭顶之灾,

比如环境恶化,地球冰期到来,大陨石撞向地球。。。,

地球迎来现有文明的灭绝,以及多个亿年后。。。可能的下一波新生文明。。。

扯远了,回到正题。

除了具备16位程序开发基础知识外,还需要了解 PXE开发知识,以及BIOS中断,其实主要是 INT 13H 磁盘中断。

PXE系统规范手册可以去intel官网搜寻。

作为开发,可以去借鉴现有的一些开源代码,比如最齐全的 ipxe,还有一些零散的。

查看开源代码,这是了解PXE接口的调用办法最快的途径。

无盘启动,顾名思义,就是系统在没有硬盘的情况下启动和运行。

其实并不是没有硬盘,而是硬盘不在本地,是通过网络连接,远在服务器上。

既然如此,那就更简单了,专门做这么一个硬件,在硬件底层上把硬盘延长了。

确实有这样的硬件,把网卡和硬盘SCSI HBA集成到一起,

对上表现为一块硬盘SCSI HBA硬盘管理器,对下则是通过以太网传输硬盘数据。

但是本文并不阐述这样的硬件设备,而是在普通网卡上,通过软件方式,实现硬盘的 “延长” 功能。

当然,软件实现也必须得到硬件支持:需要BIOS具备从网卡启动功能,需要网卡支持远程启动。

这种硬件支持,很早前就具备,BIOS有网卡启动选项,

网卡通过PXE规范支持常规系统还没运行起来时候的网络数据传输。

对于 Legacy BIOS启动过程(至于UEFI BIOS引导启动,以后有机会再阐述):

首先是BIOS上电自检,主要是查看各种连接上来的基础设备是否正常。

包括,CPU,640K基础内存,1M以上的扩展内存,显卡,键盘等等。

以及一些初始化工作后,

lagacy BIOS开始把引导权力交给引导程序,

BIOS根据配置选项,

如果是从本地硬盘引导的话,则读取磁盘的第一个扇区的MBR记录,

并把它放到内存 0000:7C00, 然后BIOS把CPU指令跳转到这个地址,之后就退居幕后,引导权交给MBR。

至于为何偏偏是这么个奇怪的地址,我想应该是跟最早的硬件设备有关,后来应该是为了兼容性性,保留这么一个地址。

后面我们会看到,引导程序基本都是放到这个地址去执行的。

MBR是一个扇区,总共512字节,前446字节是简单的引导程序,其后是4个16个字节的磁盘分区表,最后两个是结束标志 55AA。

前446字节里就是一段程序代码,它不同于我们linux,windows上的程序,它没有复杂的PE头,不需要额外解析器。

它简单到,把CPU指令指针放上去就能执行的那种裸机代码,纯粹的汇编形式的代码。

这也是我们接下来需要开发的引导程序的样子。

MBR引导程序检测4个分区表是否完善,接着找到活动分区,然后把活动分区的第一个扇区数据读入到 0000:7C00,

然后MBR把引导权交给活动分区的第一个扇区。

这个扇区的内容就跟具体的操作系统有关了,比如 winxp 操作系统,这个扇区会读取 ntldr 引导程序,

活动分区的第一个扇区也只有512字节,它也不大可能在这么小的内容上建立一个文件系统,

因此winxp操作系统应该是把 ntldr 文件复制到固定扇区中,这样这个引导扇区就能简单读取固定扇区,从而完整加载ntldr。

之后引导权再次转交给ntldr,ntldr首先读取boot.ini配置文件,建立起一个简单的文件系统,

NTFS或FAT32,为接下来读取各种配置文件和系统文件做准备。

ntldr做完各种其他操作之后,加载需要的基础组件,

更主要的就是加载驱动,属于boot阶段能启动的驱动,ntldr通过读取注册表各种服务,

找到start=0,也就是在boot阶段执行的驱动,按照注册表中指定的执行顺序加载这些驱动,

按照

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ServiceGroupOrder

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GroupOrderList

注册表中指定的group order和每个group里单个驱动的顺序。

直到这些驱动全加载到内存,ntldr整个过程都是在16位的实模式下运行,

根据我的测试,winxp会在实模式下读取至少30M多的数据, 而win7会读取 70多M,到了win10,则会读取80多M,

这个数据相对而言还是比较大,这也是考验我们即将开发的引导程序对PXE读写速度的优化情况。

ntldr把这些需要执行的基础组件,各种驱动,全部加载到内存之后,

接下来,就会把执行权力交给winxp系统,winxp会切换到64位或32位的保护模式,开始windows系统的真正启动过程。

这个时候,我们就会在显示器中看到四个花一样的图像慢慢出现,这个过程就是winxp系统执行基础组件,运行各种驱动的过程。

这其中,windows的磁盘驱动也在boot阶段启动,这样等于是windows接管了 BIOS INT13H对磁盘的读写,

因为后面的windows阶段需要大量的读写磁盘。

再然后,winxp执行完boot阶段的驱动之后,进入系统的IoInit系统初始化阶段,加载和执行的驱动更多。

系统初始化完成,接着进入windows的Logon登录阶段,至此,windows系统的启动过程顺利完成。

至于win7和win10在legacy BIOS下的启动过程基本类似,

除了有一点不同,

第一个扇区读取的是bootmgr,bootmgr再读取 system32\Boot\winload.exe,接着再来执行后面的驱动加载等过程。

以上讲了这些windows的启动过程,当然,这里阐述的是非常笼统的过程。

但是这对于我们开发引导程序以及后面windows下用于启动的虚拟磁盘驱动都有一定帮助。

讲了BIOS通过本地磁盘的启动方式,再来看看BIOS把引导权交给网卡的情况。

网卡本身有个ROM,存储的就是一些基本代码,包括引导代码,PXE相关等,

当BIOS把网卡的引导代码加载到内存,并交权给网卡引导代码之后,

网卡引导代码首先会发送DHCP广播到局域网。

这个DHCP协议与普通的DHCP稍微有些区别,但总体协议格式基本一样。

它主要不同之处,除了获取到动态IP地址之外,还需要获取引导文件名,以及TFTP服务器地址。

我们可以使用现成的dhcp服务器软件,也可以在掌握了DHCP协议规范之后,

自己实现,反正也不是难事,几百行C代码就能搞定。

在网卡获取到分配给自己的IP地址之后,同时获取到了TFTP地址,以及一个引导文件名。

在这之后,网卡开始通过 TFTP协议下载这个引导文件。

这里又要牵涉到另一个TFTP协议,一样的,在掌握了TFTP协议之后,

TFTP服务端同样可以自己实现,几百行C代码也能搞定。

我记得在CSDN上还专门记录过TFTP代码:

https://blog.csdn.net/fanxiushu/article/details/11900125?spm=1001.2014.3001.5501

网卡下载完引导文件,并且放到0000:7C00地址,之后CPU跳转到这个地址,开始执行这个引导程序。

而这个引导程序,就是我们即将需要开发的。

但是,如何让这个引导程序,和我们的无盘启动计划联系起来!

这就需要再次回顾上面阐述的从BIOS上电自检,到windows启动的全部过程。尤其是在16位的实模式下,

各种引导程序运行到交权给下一个引导程序,

不管是从MBR开始,到活动分区第一个扇区,再到ntldr亦或者是bootmgr,

这其中都会牵涉到磁盘扇区的读取,实模式下,不论是哪个程序,都是通过 BIOS 的 INT 13H中断读取的磁盘扇区数据。

如果熟悉16位编程,多少都会了解到,这些INT中断,当发生时候,CPU都会跑去特定的中断服务列表入口,执行特定的中断服务函数。

而中断服务列表,是BIOS初始化之后,就会在内存建立起来的一个地址列表。

而如果,我们把 13H中断的中断服务函数给替换掉,替换成我们的常驻内存的函数入口,

而这个函数则通过PXE通信,把磁盘读写请求重定向到服务器端。

这样不管是 MBR引导程序,ntldr,bootmgr等,通过INT 13H读写磁盘的时候,其实是通过我们的函数重定向到了服务器端。

这个就是我们开发引导程序的最主要和核心的功能。

其实,从另一个角度去理解,这就是一个在16位的实模式下另类的虚拟磁盘而已。

当然,光实现实模式下的虚拟磁盘还不够,上面windows启动过程中,当windows转到保护模式运行后,

windows就已经全部接管了所有硬件,包括磁盘硬件。因此还必须有对应的windows下的虚拟磁盘驱动,

而且根据启动过程分析,这个虚拟磁盘驱动,还必须在boot阶段就得启动起来,并且正确跟网络通信,获取服务器磁盘数据。

因此在这个windows虚拟磁盘加载之前,网卡的驱动必须先加载,并且需要网卡成功联网通信,

因为有些网卡驱动虽然加载了,但网卡硬件却处于准备状态。

这个也是最难搞的之一,因为不同网卡的windows驱动各具特色。

这些都是以后有机会再阐述的内容,

这里我们重点关注的是,16位实模式下的这个具有“虚拟磁盘”功能的引导程序的开发过程。

我们先来看一段汇编代码,

这是我大概10年前实现的,只是最近整理修改,主要是为了提高PXE通信速度。

而且是我整个引导程序中的唯一使用汇编写的代码,其他部分都是C/C++代码,

因为这是引导程序入口,没办法,只能使用汇编。

.model TINY

;------------------------------------------------------------

.386 ; CPU type

;------------------------------------------------------------

;.model TINY ; memory of model

;---------------------- C函数 -----------------------------

extrn _x7c00_Init:near ;

extrn _Int13HookEntry:near ; 申明C入口函数

extrn _PxeIsrEntry:near ;

;------------------------------------------------------------

;----------------

;------------------------------------------------------------

.code

org 0000h;; ; 0偏移对齐

;----------------------- CODE SEGMENT -----------------------

start:

cli

xor bx,bx

mov ss,bx ;初始化堆栈段

mov ss:[7C00h-2], sp ;把sp压入堆栈

mov sp, (7C00h-2)

push ds

pusha

mov ds, bx ;把DS段设置成 0

;;;调用C函数初始化

call _x7c00_Init

;;;;预留20k的数据到系统的保留内存,防止他被覆盖

mov ax, ds:[0413h]

and al, NOT 3

sub ax, 32 ;;;;修改BIOS数据区,单位KB,预留 32KB

mov ds:[0413h], ax

;sub word ptr ds:[0413h], 32 ;;;修改BIOS数据区,单位KB,预留 32KB

;mov ax, ds:[0413h]

shl ax,(10-4) ;;;获得预留的目标段

mov es, ax ;;设置正确目标段所在位置

;;;;;把自己复制到 预留内存区

cld

mov si,7C00h

xor di,di

mov cx,8000h/2 ;;复制 32K大小的数据

rep movsw

;;;;;替换 13h 中断服务程序

mov word ptr ds:[13h*4 ], int13hook

mov word ptr ds:[(13h*4) +2 ], es

;;;;;;;;;;替换 15h 中断服务程序,主要是拦截处理 e820功能

mov eax, dword ptr ds:[ 15h*4 ]

mov dword ptr es:[old_int15hook], eax ;;保存原来的地址

mov word ptr ds:[ 15h*4 ], int15hook

mov word ptr ds:[ 15h*4 +2 ], es

;;;;跳转到新地址执行

push es

push bootfromnetdisk

retf

bootfromnetdisk:

sti

;;;清屏设置显示模式

mov ax,0002h

int 10h

mov es, cx ; CX=0 from rep stosw

mov ax, 0201h ; Al=number of sector Ah=2 read sector

inc cx ; CH = cylinder; CL = sector and high bits of cylinder

mov dx, 0080h ; DH = head; DL = drive number

mov bx, 7C00h ; ES:BX dest buffer

int 13h

jc bootfromnetdisk

popa

pop ds

pop sp

db 0EAh ; JMP FAR 0000:7C00H

dw 7C00h, 0000h

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

int13hook:

;;;按照 int13_entry结构压入参数

pushf ;;保存标志

cli ;;;此操作会影响标志

push ss

push es

push ds

push cs

push di

push si

push dx

push cx

push bx

push ax

;;参数far地址压入堆栈

mov ax, sp ; int13_entry参数offset地址

push ss ;参数段地址

push ax

sti

;;;

call _Int13HookEntry ;;调用C函数

;;;;

cli

add sp, 4 ;;弹出参数far地址

;;;;;;;;

pop ax

pop bx

pop cx

pop dx

pop si

pop di

add sp, 2 ;;;pop cs

pop ds

pop es

pop ss

popf

;;;;

retf 2 ;;返回这里不恢复flags,而是直接 sp +2

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

以上代码大概意思是,在引导程序被加载到0000:7C00之后,

执行 x7c00_Init 初始化,这是个C函数,在里边其实主要是初始化PXE,获取PXE接口等。

然后把整个引导程序复制到 BIOS的0413保留区,这样在防止被后面的程序覆盖,因为我们是常驻程序,

是需要提供13H中断服务的,代码中预留的保留区有点大,32KB,

这个根据自己的引导程序大小可以动态计算,但是为了简单,这里就这么粗暴的预留了32KB。

接着就是重头戏,替换13H中断服务函数,替换成 int13hook,

而int13hook在经过一些列压栈,最终调用C函数 Int13HookEntry,

Int13HookEntry则是整个的核心,它完成磁盘所有功能,通过PXE把磁盘读写发送到服务器端。

完成这些初始化之后,接着让程序直接转到 bootfromnetdisk,

这个bootfromnetdisk过程其实就是直接通过INT 13H,读取MBR到 0000:7C00,

但是这个时候13H中断已经被我们替换,它最终会进入到 Int13HookEntry函数,

因此最终读取服务器上的磁盘数据,

未完待续,下面会接着讲述这个引导程序的磁盘开发部分以及PXE通信过程。。。

相关文章:

windows无盘启动技术开发之传统BIOS(Legacy BIOS)引导程序开发之一

by fanxiushu 2023-03-01 转载或引用请注明原始作者。这个话题可能有点老,UEFI BIOS 已经大量存在,而Legacy BIOS最终会被取代。但是也是作为无盘启动技术里不可或缺的,毕竟还有许多老型号的电脑存在,而且为了兼容性,有…...

mysql实现if语句判断功能的六种使用形式

文章目录 前言一、ifnull函数二、nullif函数三、if函数四、if语句(多用于存储过程)五、if-else语句(多用于存储过程)六、if-elseif-else语句(多用于存储过程)总结前言 在Mysql数据库中实现判断功能有很多方式,具体又分为函数和if语句形式,函数的好处是可以作为sql的一…...

在Vue3这样子写页面更快更高效

前言 在开发管理后台过程中,一定会遇到不少了增删改查页面,而这些页面的逻辑大多都是相同的,如获取列表数据,分页,筛选功能这些基本功能。而不同的是呈现出来的数据项。还有一些操作按钮。 对于刚开始只有 1&#xff…...

做软件测试,如何才能实现月入20K?

听我的,测试想要月入20k。 首先你要去大厂,不在大厂起码也得在一线城市,北上广深。 二线城市的话成都、杭州最好。 不然的话想都不要想。 像我之前整理过成都的公司,除了字节跳动、蚂蚁金服、滴滴、美团、京东、平安、字节跳动…...

mysql last lesson

1:创建用户 create user zhang identified by 12345678;2:给用户授权,撤销授权, grant.......to revoke ....... 3:将数据库中的数据导出 C:\Windows\system32>mysqldump bjpowernode>C:\bjpowernode.sql -uroot -p12345678 4&#…...

一、Redis入门概述(是什么,能干嘛,去哪下,怎么玩)

一. redis是什么? Redis:REmote Dictionary Server(远程字典服务器)官方解释: Remote Dictionary Server(远程字典服务)是完全开源的,使用ANSIC语言编写遵守BSD协议,是一个高性能的Key-Value数据库提供了丰富的数据结构&#xff…...

(六十二)当我们在SQL里进行分组的时候,如何才能使用索引?

今天我们接着上次的内容来谈谈在SQL语句里假设你要是用到了group by分组语句的话是否可以用上索引,因为大家都知道,有时候我们会想要做一个group by把数据分组接着用count sum之类的聚合函数做一个聚合统计。 那假设你要是走一个类似select count(*) fr…...

python字符串练习

python字符串练习 1.去掉字符串中所有的空格 s This is a demo print(s.replace( , )) 2.获取字符串中数字的个数 data input("请输入一些字符串:") a 0 for i in data:if i.isdigit():a a 1 print("数字个数:", a)3.将字母全部转换为…...

Java-封装、继承、多态

封装 访问控制权限又成为“封装”,是面向对象三大特征中的一种。核心是,只对需要的类可见。 继承 继承是所有OOP(Object Oriented Programming)语言和Java语言都不可或缺的一部分。 只要创建一个类,就隐式继承自Obje…...

问题三十二:离散二维傅立叶变换(Discrete Fourier Transformation)

为了将灰度图像表示为频谱图,我们需要进行以下步骤: 加载图像并将其转换为灰度图像。对图像进行二维离散傅里叶变换。将变换结果表示为幅度谱和相位谱。可以对幅度谱和相位谱进行可视化,以查看频率分布。对幅度谱和相位谱进行逆变换&#xf…...

恢复谷歌翻译的究极方法

谷歌翻译为什么会失效,我想各位在去年11月的时候就知道了。可是要怎么解决失效的问题呢?之前我们是通过手动Ping可以连接的ip各位可能觉得麻烦,心里觉得什么档次还要我手动ping就没有可以自动扫描的吗?还别说真的有我最近发现一个…...

string函数以及string常用接口

本文介绍的是C关键字string中一些重要用法,以及各种字符串序列的处理操作 ——飘飘何所似,天地一沙鸥 文章目录前言一、string(字符串类)二、string类对象的容量操作2.1 size/length2.2 capacity2.3 empty/clear2.4 resize/reser…...

分享一篇由C语言实现《数据结构》无头无循环单链表

三月,你好,各位csdn uu们好 文章目录前言一、何为单链表二、单链表基本操作(增,删,查,改,销毁,遍历)1.查找与修改、销毁与遍历2.链表插入与删除操作三、单链表 VS 顺序表…...

C盘爆满?两个超简单的解决办法

我们在使用电脑的过程中,经常容易出现C盘爆红,反而其他盘还有大量可用空间的情况。为什么会这样呢?其实主要就两种原因:一是电脑使用习惯不好,不管什么软件都默认安装在C盘,大文件又喜欢放在桌面&#xff0…...

ThreadLocal

ThreadLocalThreadLocalMapgetsetremove内存泄漏key用强/弱引用entry继承了弱引用ThreadLocal 一个对象的所有线程会共享其全局变量——>线程不安全 解决方式: 方式一:同步机制,加锁(时间换空间) 方式二&#xff1a…...

Java基础:JDK7-时间Date

JDK7以前时间相关类 1.Date Date date new Date(); , sout(date)得到的是现在所处位置的时间 Date date new Date(0L); , sout(date)得到的是时间原点也就是1970年1月1日08:00(东八区). date.setTime(1000L); sout(date)得到的是时间原点后一秒钟的时间 long time date.g…...

什么是IP地址?

IP协议中还有一个非常重要的内容,那就是给因特网上的每台计算机和其它设备都规定了一种地址,叫做“IP 地址”。由于有这种地址,才保证了用户在连网的计算机上操作时,能够高效而且方便地从千千万万台计算机中选出自己所需的对象来。…...

4年经验之谈,什么是接口测试?怎样做接口测试?

一、什么是接口?【文末学习资源分享】赶紧嫖!冲!!!! 接口测试主要用于外部系统与系统之间以及内部各个子系统之间的交互点,定义特定的交互点,然后通过这些交互点来,通过…...

普通指针扫盲

一、什么是指针 C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址。 CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存 中的一个指定数据…...

深度学习笔记:神经网络权重确定初始值方法

神经网络权重不可为相同的值,比如都为0,因为如果这样网络正向传播输出和反向传播结果对于各权重都完全一样,导致设置多个权重和设一个权重毫无区别。我们需要使用随机数作为网络权重 实验程序 在以下实验中,我们使用5层神经网络…...

利用ngx_stream_return_module构建简易 TCP/UDP 响应网关

一、模块概述 ngx_stream_return_module 提供了一个极简的指令&#xff1a; return <value>;在收到客户端连接后&#xff0c;立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量&#xff08;如 $time_iso8601、$remote_addr 等&#xff09;&a…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

聊聊 Pulsar:Producer 源码解析

一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台&#xff0c;以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中&#xff0c;Producer&#xff08;生产者&#xff09; 是连接客户端应用与消息队列的第一步。生产者…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢

随着互联网技术的飞速发展&#xff0c;消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁&#xff0c;不仅优化了客户体验&#xff0c;还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用&#xff0c;并…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

vue3+vite项目中使用.env文件环境变量方法

vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量&#xff0c;这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程

本文较长&#xff0c;建议点赞收藏&#xff0c;以免遗失。更多AI大模型应用开发学习视频及资料&#xff0c;尽在聚客AI学院。 本文全面剖析RNN核心原理&#xff0c;深入讲解梯度消失/爆炸问题&#xff0c;并通过LSTM/GRU结构实现解决方案&#xff0c;提供时间序列预测和文本生成…...

AspectJ 在 Android 中的完整使用指南

一、环境配置&#xff08;Gradle 7.0 适配&#xff09; 1. 项目级 build.gradle // 注意&#xff1a;沪江插件已停更&#xff0c;推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...