从零编写linux0.11 第一章 简单的bootloader_gas bootloader-程序员宅基地

技术标签: linux0.11  linux  操作系统  

编程环境:Ubuntu Kylin 16.04

代码仓库:https://gitee.com/AprilSloan/linux0.11-project

工程结构:每一个目录对应于一章的内容,如chapter_01对应于第一章。每一章包含多个小节,如chapter_01/1st代表第一章第一节(简单的启动盘),所有的shell命令都默认当前目录为小节的目录。

linux0.11源码下载(不能直接编译,需进行修改)

原本linux0.11是用gas汇编编写bootloader,我认为大家对gas汇编不太熟悉,所以用Intel汇编编写bootloader。部分汇编知识和计算机知识并不会详细讲述,毕竟这是博客不是写书,还请大家见谅。

1.简单的启动盘

来写一个简单的启动盘吧。启动盘有两个最基本的要求,一是大小必须为512字节,二是最后两个字节必须是0x55和0xaa,不然这不会被识别为启动盘。

以下为bootsect.s的内容。

start:
	jmp start		; 死循环

	times 0x1fe - ($ - $$) db 0	; 填写0,直到0x1fe
	dw 0xaa55		; 启动盘标识

第1、2行是死循环,第4行是让jmp指令后到0x1fe的空间全部填充为0。第五行是让地址0x1fe和0x1ff分别为0x55和0xaa。

利用如下指令编译汇编文件。(没有安装nasm的话,用sudo apt install nasm安装nasm)

nasm boot/bootsect.s -o boot/bootsect.bin

此时,bootsect.bin就可以用来仿真调试了,但为了规范,我们还是将bootsect.bin写入软盘中,再用软盘启动。所以,我们要创建一个软盘。这里使用bximage创建软盘,安装bochs仿真器的过程中就会安装bximage,大家可以看看我的另一篇博客安装bochs:Linux下bochs的安装与使用,里面也有使用bochs的方法。

bximage创建软盘

接下来要将bootsect.bin写入kernel.img中,使用如下命令:

dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc

现在就可以用kernel.img进行仿真调试了。但还别急,要使用bochs仿真还需要配置文件固定仿真使用的cpu、内存大小、软盘启动设置等等,这个文件在仓库中有,我命名为bochsrc,内容如下。

plugin_ctrl: unmapped=1, biosdev=1, speaker=1, extfpuirq=1, parallel=1, serial=1, iodebug=1
config_interface: textconfig
#使用GUI调试
display_library: x, options="gui_debug"
cpu: model=core2_penryn_t9600, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"
cpu: cpuid_limit_winnt=0
cpuid: x86_64=1, mmx=1, sep=1, simd=sse4_2, apic=xapic, aes=1, movbe=1, xsave=1
cpuid: family=6, model=0x1a, stepping=5
romimage: file=$BXSHARE/BIOS-bochs-latest 
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest
vga: extension=vbe, update_freq=5
keyboard: type=mf, serial_delay=250, paste_delay=100000, user_shortcut=none
mouse: type=ps2, enabled=0, toggle=ctrl+mbutton
pci: enabled=1, chipset=i440fx
clock: sync=none, time0=local, rtc_sync=0
private_colormap: enabled=0

#软盘启动的配置,kernel.img为软盘名
floppya: type=1_44, 1_44=kernel.img, status=inserted, write_protected=0

ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata1: enabled=0, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata2: enabled=0, ioaddr1=0x1e8, ioaddr2=0x3e0, irq=11
ata3: enabled=0, ioaddr1=0x168, ioaddr2=0x360, irq=9

# 硬盘启动配置,kernel.img为硬盘名,根据硬盘大小不同需改变后三个参数的值
#ata0-master: type=disk, mode=flat, path=kernel.img, cylinders=130, heads=16, spt=63
#ata0-slave: type=none
#ata0-master: type=none
#ata1-slave: type=none

# 软盘启动/硬盘启动
boot: floppy
# boot: disk

floppy_bootsig_check: disabled=0
log: -
logprefix: %t%e%d
panic: action=ask
error: action=report
info: action=report
debug: action=ignore
debugger_log: -
com1: enabled=1, mode=null
com2: enabled=0
com3: enabled=0
com4: enabled=0
parport1: enabled=1, file=none
parport2: enabled=0
speaker: enabled=1, mode=system
magic_break: enabled=0
print_timestamps: enabled=0
port_e9_hack: enabled=0
megs: 2048

好了,开始仿真调试吧!

bochs -qf bochsrc

输入以上命令会出现两个界面,如下所示:

bochs gui调试界面
bochs终端界面

第一个界面会显示操作系统的汇编代码,当前的寄存器数值,可以查看内存内容,输入指令控制程序运行。第二个程序是操作系统运行时用于显示的界面。

在第一个界面中分别输入以下两个指令:

b 0x7c00
c

第一个指令用于在0x7c00地址打断点,第二个指令让程序继续运行,程序会在断点处停止。界面会变成如下所示:

0x7c00地址的界面

这是我们编写的代码。我们可以看到,程序被加载到了0x7c00地址处,那为什么是在这个地址呢?这是约定俗成的,电脑开机,会将磁盘的前512字节内容拷贝到0x7c00地址,如果这512字节不是以0x55和0xaa结束,会报错说找不到启动盘。

这一节的内容就到此为止了,下一节让我来打印Hello World吧。

2.打印Hello World

学习编程必不可少的是什么?是语法或数据结构吗?不,是Hello World!

我们对bootsect.s进行修改。

start:
	mov ax, 0x07c0
	mov es, ax
	mov dx, 0		; 光标位置为(0,0)
    mov cx, 16		; 写16个字符
    mov bx, 0x0007	; 页面0,颜色模式7
    mov bp, msg		; 字符串地址
    mov ax, 0x1301	; 写字符串,光标随之移动
    int 0x10		; 进入BIOS中断

	jmp $			; 死循环

msg:				; 要打印的字符串
    db 13, 10
    db "Hello World!"
    db 13, 10

	times 0x1fe - ($ - $$) db 0	; 填写0,直到0x1fe
	dw 0xaa55		; 启动盘标识

这段程序打印Hello World!后进入死循环。虽然我把程序执行结果告诉你了,但你还是想知道第2-8行是什么意思对不对?推荐下载BIOS接口技术参考手册,这是我的资源,不收C币,免费下载。查阅手册,找到int 0x10下的如下内容:

int_0x10_ah_0x13

虽然是英文的,但也不难。汇编程序中改变了ax,bx,cx,dx,es,bp等寄存器的值,改变这些寄存器的目的都在上图中指出。

最后得到的结果如图所示。

打印Hello World的结果

有没有觉得每次在终端里敲命令很繁琐?就不能一条命令就编译内核然后启动仿真器吗?当然可以!现在轮到脚本和Makefile出场了。

使用bximage制作软盘虽然只需要敲几下键盘就可以了,但本着能偷懒就偷懒的精神,我用脚本(在Makefile中出错了)实现了制作软盘的步骤,命名为mkimg.sh。

#!/bin/bash
RED_COLOR='\E[41m'
BLUE_COLOR='\E[44m'
RESET='\E[0m'
echo -e "${BLUE_COLOR}=== env check ===${RESET}"
if [ ! -e bochsrc ];then
    echo -e "${RED_COLOR}=== no bochsrc ===${RESET}"
    exit 1
fi

if [ ! -e /usr/local/bin/bochs ];then
    echo "${RED_COLOR}=== no bochs ===${RESET}"
    exit 1
fi

if [ ! -e /usr/local/bin/bximage ];then
    echo "${RED_COLOR}=== no bximage ===${RESET}"
    exit 1
fi

if [ -e kernel.img ]; then
    rm kernel.img
fi

echo -e "1\nfd\n\nkernel.img\n" | bximage

我在这里面添加了不少提示性的信息,最后一句才是精华,好好品味吧。

Makefile要完成所有的工作,之后,一条make指令就可以开始仿真调试了。

default: all

all: Image

Image: mkimg boot/bootsect.bin
	dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
	bochs -qf bochsrc

boot/bootsect.bin: boot/bootsect.s
	nasm boot/bootsect.s -o boot/bootsect.bin

mkimg:
	./mkimg.sh

clean:
	rm -rf boot/*.bin kernel.img

这个Makefile并不难理解(每次有人说**不难理解,我都想打人,我终究活成了自己讨厌的摸样:-),不做过多赘述。

这节内容到此结束,下节会详细讲讲启动盘的任务。

3.boot和loader

bootloader的作用是什么?它主要完成加载内核以及系统初始化,把加载内核的工作称为boot,把系统初始化称为loader。bootsect.s完成boot部分,setup.s完成loader部分。

系统一开始只会将bootsect.s的内容,而setup.s的内容就需要让bootsect.s加载到内存中。我们首先将bootsect.s的内容从0x7c00移动到0x90000,再将setup.s的内容加载到0x90200。前面0x00000~0x90000的空间之后会用来存放内核。这里我们假设setup.s的内容会占用4个扇区。

下面会小小地改动bootsect.s的内容。

SETUPLEN    equ 4
BOOTSEG     equ 0x07c0
INITSEG     equ 0x9000
SETUPSEG    equ 0x9020

start:
    mov ax, BOOTSEG
    mov ds, ax
    mov ax, INITSEG
    mov es, ax
    mov cx, 256
    sub si, si
    sub di, di
    rep
    movsw			; 将bootsect.s从0x7c00移动到0x90000
    jmp INITSEG:go
go: mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0xff00

load_setup:
    mov dx, 0x00
    mov cx, 0x02
    mov bx, 0x0200
    mov ax, 0x0200 + SETUPLEN
    int 0x13		; 加载setup.s到0x90200
    jnc ok_load_setup
    mov dx, 0x00
    mov ax, 0x00
    int 0x13
    jmp load_setup	; 加载失败则复位软盘,重新加载

ok_load_setup:
    mov ah, 0x03
    xor bh, bh
    int 0x10		; 获取光标位置

    mov cx, 24
    mov bx, 0x0007
    mov bp, msg
    mov ax, 0x1301
    int 0x10		; 打印字符串

    jmp SETUPSEG:0	; 跳转到setup.s的内容

msg:
    db 13, 10
    db "Loading system ..."
    db 13, 10, 13, 10

    times 0x1fe - ($ - $$) db 0 ; 填写0,直到0x1fe
    dw 0xaa55       ; 启动盘标识

这个内容不算多吧。。。前4行定义了4个宏定义,方便以后修改或维护。第7~15行将移动bootsect.s的内容,rep movsw会将[ds:si]地址的两个字节移动到[es:di]处,每次移动si和di都加1,一共移动cx次。然后是跳转到移动后的地址。

跳转后要重新设置段寄存器和sp,这些寄存器在之后都会用到,sp的值只要远大于512即可。

做完这些就开始加载setup.s的内容,setup.s的内容保存在第2~5扇区内容。查BIOS接口技术参考手册的int 0x13可以知道:

int_0x13_ah_2

dh=0,dl=0,ch=0,cl=2代表第2扇区,al=4代表读取4扇区,将内容读到es:bx(0x9000:0x200)地址处。读取成功,CF=0,跳转到ok_load_setup;读取失败,CF=1,复位软盘,重新读取扇区。

下面是读取光标位置到dx寄存器中。在第2节中,打印Hello World的地方原本有字符,不大好看。读取光标位置后打印字符串会打印在空白地区,更好看一点。

打印字符串的程序之前已经说过,就不多说了。打印Loading system …之后就会跳转到setup.s的内容中。

这次把setup.s写简单一点,还是一个死循环。

start:
    jmp start

弄完bootsect.s和setup.s后,就开始更改Makefile。

default: all

all: Image

Image: mkimg boot/bootsect.bin boot/setup.bin
	dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
	dd if=boot/setup.bin of=kernel.img bs=512 count=4 seek=1 conv=notrunc
	bochs -qf bochsrc

boot/bootsect.bin: boot/bootsect.s
	nasm boot/bootsect.s -o boot/bootsect.bin

boot/setup.bin: boot/setup.s
	nasm boot/setup.s -o boot/setup.bin

mkimg:
	./mkimg.sh

clean:
	rm -rf boot/*.bin kernel.img

dd指令的使用可以在linux中使用dd --help进行查看,其余就没什么好讲的了。

那就开始仿真调试吧。

make完运行了jmp INITSEG:go指令之后,地址发生了变化。

jmp_0x90018

我们可以一直使用n命令一步一步运行。

Loading_system

这次打印的字符串明显要更好一些。

Refresh

咦?说好的setup.s的内容呢?别慌,点击上面的Refresh键。

Refresh之后

这次可以看到死循环了。

这一节的内容结束了,下一节会开始完善setup.s,bootsect.s的内容会在开始写内核的时候再完善。

4.完善setup.s

setup.s要本要完成移动内核到指定位置的任务,但现在还没有写内核,就只完成初始化的功能就可以了。

目前,CPU还处于16位模式,能够使用的寄存器也都是16位的,而我们接下来要进入32位保护模式,setup.s会为进入32位做准备。(我们都知道32位比16位好,那这是为什么呢?)

要进入保护模式需要做什么呢?

  • 初始化GDT描述符,加载gdtr
  • 打开A20地址线
  • 设置cr0寄存器的PE位为1,使之运行于保护模式

最后还需要跳转到保护模式的地址,但由于没写内核代码,这步将由死循环代替(老惯例了)。

以下是修改后的setup.s的代码:

INITSEG     equ 0x9000
SYSSEG      equ 0x1000
SETUPSEG    equ 0x9020
	
	cli		; 保护模式下中断机制尚未建立,应禁止中断
	
start:    
    mov ax, SETUPSEG
    mov ds, ax
    lgdt    [gdt_48]

    mov al, 2
    out 0x92, al

    mov ax, 0x0001
    lmsw    ax
    jmp $

gdt:
    dw  0, 0, 0, 0
    dw  0x07ff, 0x0000, 0x9a00, 0x00c0
    dw  0x07ff, 0x0000, 0x9200, 0x00c0

gdt_48:
    dw  0x800
    dw  512 + gdt, 0x9

gdt包含操作系统内存分段管理的相关知识,在setup.s中只是临时设置gdt,在内核中会重新设置gdt。gdt的结构如下所示。如果对gdt不感兴趣就跳过下面的图表吧:-)

gdt结构图

请添加图片描述

gdt各字段解释

gdt各字段 意义
基地址 段在内存中的起始地址
段界限 段长度=(段界限+1)*段界限单位(段界限单位与下方的G字段有关)
G 粒度位,用于解释段界限的含义。G=0,段界限以字节为单位,段的扩展范围为1B到1MB(描述符的界限值为20位);G=1,段界限以4KB为单位,段的扩展范围为4KB到4GB
D/B 默认的操作数大小。主要是为了能够在32位处理器上兼容运行16位保护模式的程序。D/B=0,表示指令中的偏移地址或者操作数是16位的;D/B=1,表示指令中的偏移地址或者操作数是32位的。
L 64位代码段标志,保留此位给64位处理器使用,目前将此位置0即可。
AVL 软件可利用位。80386对该位未作规定,且与80386兼容的处理器都不会对该位的使用做任何规定。
P 段存在位,用于表示描述符所对应的段是否存在。P=0,表示段不在内存中。P=1,表示段在内存中。
DPL 表示描述符的特权级。处理器支持的特权级别有4种,分别是0,1,2,3,其中0是最高特权级别。
S 用于指定描述符的类型。S=0,表示这是一个系统段;S=1,表示这是一个代码段或数据段(堆栈段也是特殊的数据段)。
TYPE 由于介绍过长,放在下面解释

gdt中TYPE字段的介绍

数据段 代码段
X E W A 含义 X C R A 含义
0 0 0 X 只读 1 0 0 X 只执行
0 0 1 X 读和写 1 0 1 X 读和可执行
0 1 0 X 只读,向下扩展 1 1 0 X 只执行,依从的代码段
0 1 1 X 读写,向下扩展 1 1 1 X 可执行,读,依从的代码段

上表中第2行各字母的含义

字母 含义
X 表示是否可执行。X=0,表示不可执行,数据段总是不可执行的;X=1,表示可执行,代码段总是可执行的。
E 段的扩展方向。E=0,向上扩展,即向高地址方向扩展,是普通的数据段;E=1,向下扩展,即向低地址方向扩展,通常是堆栈段。
W 段的写属性。W=0,不允许写入,此时写入的话会引发异常中断;W=1,允许写入。
A 已访问位。用于表示它指向的段最近是否被访问过。在描述符创建的时候应该清0。之后每当该段被访问时,处理器将该位置1。
C 段特权级依从。C=0,表示非依从的代码段,可以与特权级相同的代码段调用,或者通过门调用;C=1,表示运行从低特权级的程序转移到该段执行。
R 段是否允许读出。R=0,表示代码段不能读出,此时读出会引发处理器异常中断;R=1,表示代码段可以读出。

通过上面的图表,我们可以知道代码第18-20行都干了什么。每个gdt占8个字节,所以每一行就是一个gdt。第一个gdt内容必须是0,对应于第18行代码。第二个gdt代表系统代码段,这8个字符的意思是:该段在内存的起始地址是0,段界限为0x7FF,段界限以4KB为单位,段长度为8MB,该段指令中的偏移地址或者操作数是32位的,段在内存中,特权级为0,这是一个代码段或数据段,该段可读可执行。这么介绍确实有些繁琐,不过应该挺易懂的。第三个gdt代表系统数据段,相关内容还请各位自行观看。

第8行代码是加载gdt的地址及长度到gdtr中。LGDT指令是将源操作数中的值加载到全局描述符表格寄存器(GDTR)。源操作数指定6字节内存位置,它包括了GDT的基址和界限。如果操作数大小属性是32位,则将16位限制(操作数的2个低位字节)与32位基址(操作数的4个高位字节)加载到寄存器。第24行的基址也可表示为0x90200+gdt或0x9020:gdt。

第10-11行是为了开启A20,这样就可以访问1MB以上的内容地址了。关于A20的更多知识请自行百度。开启A20的方法有几种,这里使用的是最简单的方法,与linux0.11的代码并不相同。

第13-14行是为了设置cr0寄存器的PE位为1(PE位位于cr0寄存器的bit0)。这段代码与如下代码的意思相同。

	mov ax, cr0
	or	ax, 1
	mov cr0, ax

下面来运行一下代码吧。

0x90200

这好像没什么可以展示的啊。在运行了lgdt和lmsw指令之后可以看看gdtr和cr0寄存器的变化。

进入保护模式的准备已经做好了,下面就可以写内核,但是写完内核就需要更新bootsect.s和setup.s的内容,好复杂啊。。。

这章内容有点多,感觉应该写得更细一点,但写多了又感觉会变得很臃肿,就这样了,下章的内容也不少啊。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/mondaiji/article/details/123112400

智能推荐

Rose双机热备两款软件原理介绍以及共享存储双机热备方案和镜像双机热备方案介绍_rose主备切换-程序员宅基地

文章浏览阅读5.1k次,点赞2次,收藏7次。一. RoseHA的工作原理  RoseHA双机系统的两台服务器(主机)都与磁盘阵列(共享存储)系统直接连接,用户的操作系统、应用软件和RoseHA高可用软件分别安装在两台主机上,数据库等共享数据存放在存储系统上,两台主机之间通过私用心跳网络连接。配置好的系统主机开始工作后,RoseHA软件开始监控系统,通过私用网络传递的心跳信息,每台主机上的RoseHA软件都可监控另一台主机的状态。当工作主机..._rose主备切换

Redis缓存数据库SaaS多租户实现方案-程序员宅基地

文章浏览阅读2.7k次。一、前言上2个章节已经实现了mysql和MongoDB的多租户切实现方案,本章将继续学习Redis的多数据源切换。Redis服务器默认有16个database,我们可以将每个租户的数据放到其中一个database中,也可以部署多台Redis服务器,每个租户使用一个Redis服务器,也可以把两者结合起来,Redis服务器部署多台,先在一台的16个Database上放,放满了16个Database然后再往下一台Redis服务器上放。这种方式需要有一个MySQL数据库表存储每台Redis服务器的Databa

win10触摸键盘TabTip软件特性-程序员宅基地

文章浏览阅读2.3k次。win10触摸键盘通过::SendMessage隐藏方式没有效果HWND hWnd = ::FindWindow(L"OSKMainClass", NULL);if ( hWnd ){::SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);} win10触摸键盘无法找到状态窗口状态,isWidowsVisible,GetWindowPlacement,GetWindowLong,状态没有变化 ..._tabtip

android实时声音信号波形_【每日一题】(八上)通过波形图比较声音的特性(学苑帮你成长一每日一题精析)9月25日...-程序员宅基地

文章浏览阅读1.5k次。9月25日 通过波形图比较声音的特性中考频度:★★☆☆☆难易程度:★☆☆☆☆(2019·广东初二期末)把频率为256 Hz音叉发出的声音信号输入示波器,示波器展现的波形如图甲所示。若把频率为512 Hz音叉发出的声音信号输入同一设备的示波器,其波形可能图中的__________。【参考答案】丁【试题解析】由题知,原来音叉发出声音的频率为256 Hz,现在音叉发出声音的频率为512 Hz..._规格为256hz音叉声音波形如图所示,将512hz音叉的声音输入同一设置的示波器后,其波

关于Win10安装SQLServer后在程序中不能访问的解决方法_msvcr120 sqlservr windows 无非是访问-程序员宅基地

文章浏览阅读8.1k次。前两天刚在win10中安装完SqlServer 2008 R2 ,安装步骤可以参见这篇文章,装完之后打开 Sql Management Studio,登陆、查询、用Navicat连接都没有问题,于是开始转移以前的项目到本机。然后就出现问题了,在访问项目的时候只要是关于查询数据库的程序全都卡半天,最后报个(org.hibernate.exception.GenericJDBCException: C..._msvcr120 sqlservr windows 无非是访问

Json文件格式化方法_json格式化-程序员宅基地

文章浏览阅读5.3w次,点赞2次,收藏16次。本文详细介绍了 JSON 文件格式化的方法。通过深入探讨,文中提供了多种有效的方式来对 JSON 文件进行格式化,以提高其可读性和可维护性。这些方法涵盖了使用特定工具或的相关技巧和要点。读者可以从中了解到如何快速、准确地对 JSON 文件进行格式化,以便更好地理解和处理其中的数据。_json格式化

随便推点

数学:求欧拉函数算法模板-程序员宅基地

文章浏览阅读499次。数学:求欧拉函数算法模板求欧拉函数求欧拉函数int phi(int x){ int res = x; for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) { res = res / i * (i - 1); while (x % i == 0) x /= i; } if (x > 1) res = res / x * .

matplotlib绘图时横纵坐标和图例的字体大小如何设置_matplotlib绘图横纵坐标设置-程序员宅基地

文章浏览阅读8.8k次,点赞3次,收藏8次。横纵坐标字体大小调节:通过fontsize可以进行调节ax1.set_ylabel("AUC",fontsize=20)ax2.set_ylabel("Logloss",fontsize=20)图例字体大小调节:在plt.legend中加一个prop={"size":18,"weight":"black"}即可_matplotlib绘图横纵坐标设置

HTML5特效按钮_html5 特效按钮-程序员宅基地

文章浏览阅读1w次,点赞2次,收藏19次。作为前端开发者,我们肯定都使用过非常多的jQuery插件,毋庸置疑,jQuery非常流行,尤其是结合HTML5和CSS3以后,让这些jQuery插件有了更多地动画效果,更为绚丽多彩。下面分享了一些超炫酷的jQuery/HTML5应用,一起来看看。1、HTML5/CSS3一组可爱的3D按钮这是一款利用HTML5和CSS3制作而成的按钮组合,这款CSS按钮非常具有个性化。该CSS3按钮_html5 特效按钮

树莓派(Raspberry Pi 4)开启和连接蓝牙_树莓派连接蓝牙耳机并使用麦克风-程序员宅基地

文章浏览阅读1.8w次,点赞4次,收藏43次。参考连接: link.1、查看树莓派蓝牙开启状态_树莓派连接蓝牙耳机并使用麦克风

Python3输入输出与字符串格式化_%s.%d' %()-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏12次。介绍了输入(input)、输出(print),及字符串格式化(F-string、format与%)方式_%s.%d' %()

EL表达式比较字符串或是数字格式的数值是否相等,为true,却不执行为true时的代码_el 表达式 判断字符串和数字相等-程序员宅基地

文章浏览阅读9.3k次。问题:EL表达式比较字符串或是数字格式的数值是否相等,为true,却不执行为true时的代码。示例:true原因:有可能是test="${ 1 == 1}(这里多个空格)",即大括号与双引号之间多了空格,这个时候,就不会打印true。去掉多余的空格就可以了_el 表达式 判断字符串和数字相等