拼一个自己的操作系统(SnailOS 0.03的实现)

news2024/11/26 0:39:26

像文本模式一样显示字符串

在拼操作系统的征程中,仅仅是画上一些简单的图形,显然是不够的。原因就在于,如果开发的过程中,出现了“臭虫”,而系统并不能显示任何有价值的信息,那我们岂不是两眼一抹黑,要抓瞎了。在这一章中,我们就要不遗余力地使操作系统在图形模式下,能够像文本模式一样显示字符串,从而使操作系统的开发变地简单易行。

首先看我们新添加的工具软件,这两个软件分别是makefont.exe和bin2obj.exe,它们分别是将现成的可见字库转变成二进制文件和进一步将二进制文件转变成可链接的目标文件的小程序。

在图形模式下,怎么描画和显示我们通常的字符呢?其实这是个很简单的事情了,它的本质和画矩形简直是一样一样的呢。

我们来对kernel.c所作的修改。

【kernel.c 节选】

(上面省略)

char pic[16][16] =

{

{'*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','#','#','#','#','#','#','#','#','#','#','#','#','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','#','#','#','#','#','#','#','#','#','#','#','#','#','*'},

{'*','*','#','*','*','*','*','*','#','*','*','*','*','*','#','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','#','*','*','*','*','*','*','*'},

{'*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*'},

};

unsigned int pic_data[16][16];

for(j = 0; j < 16; j++) {

for(i = 0; i < 16; i++) {

if(pic[j][i] == '#') {

pic_data[j][i] = 0x00888888;

}

if(pic[j][i] == '*') {

pic_data[j][i] = 0x00000000;

}

}

}

for(j = 0; j < 16; j++) {

for(i = 0; i < 16; i++) {

draw_point(video + 1024 * (128 + 16 + 16),

1024, i + 512, j, pic_data[j][i]);

}

}

(下面省略)

通过看上面的程序,相信大家已经清楚的看到了吧。对了,图中用字符拼出来的正是“中国”的“中”字,我们定义了两个二位数组,第一个是存放字符的,第二个则是存放颜色数值的。第一次的循环语句借助条件判断,将字符转化为颜色值,第二次是用画点函数将这个中文字符写在屏幕上。见下图。

图中的“中”字是小了点,但是一点也不影响我们的判断。同时我们看到,我们的蜗牛系统不但在信息区显示了英文字符,而且在图形区写出了“我爱你蜗牛操作系统!”的汉字,这些都是我们系统的字符处理模块的功劳。下面让我们来逐个看一下字符处理函数。我们当然是在工作目录下新建了叫做string的目录,并创建了string.h和string.c文件。

【string.h】

// string.h 创建者:至强 创建时间:2022年8月

#ifndef __STRING_H

#define __STRING_H

int pos;

void make_ascii(int* buf, unsigned int x_size, int x, int y,

unsigned int colour, char* font);

void putfont8(int* buf, unsigned int x_size, int x, int y,

unsigned int colour, char* font);

void put_str_buf(int* buf, unsigned int x_size, int x, int y,

unsigned int colour, char* str);

void put_str(char* s, unsigned int colour);

void put_str_red(char* s);

unsigned int strlen_(const char* s);

unsigned int div_(unsigned int* i, unsigned int n);

void i2a(char* buf, unsigned int a, unsigned int b);

int vsprintf_(char* buf, const char* format_s, char* temp);

int printf_(const char* format_s, ...);

void memcpy_(void* d_, void* s_, unsigned int c);

void memset_(void* d_, unsigned char v, unsigned int l);

char* strcpy_(char* a_, char* b_);

void put_gb2312_buf(int* buf, int xsize, int x, int y, int colour,

unsigned char* s);

void putfont16(int* buf, int xsize, int x, int y, int colour, short* font);

#endif

【string.c】

// string.c 创建者:至强 创建时间:2022年8月

#include "global.h"

#include "string.h"

#include "x.h"

/*

描画一个ASCII字符的函数。一个ASCII字符被定义一个字节

的长度,他被称为"美国信息交换标准码",什么又是老美。

哎,没办法呀,谁让人家计算机发展得早。为了能够画出一

个好看而且完整的字符,我们选定了8 * 16的矩形区域(点

阵)。而为了节省字库的空间在描述每个点时用计算机中的

一位来表示。即使这样,一个字符也需要16字节的内存空间。

而ASCII可表示的字符极限时256(2^8)个,因此字库的大小

推测应该最小是4096(256 * 16)字节。而实际上还要比这个

稍大一些。

*/

/*

这样大家就应该明白或者糊涂了吧,是的我在最开始的时候

也是这样。其实ASCII字符在计算机中只是一些数值,从0开

始到255也就是这个范围。为了和描绘图形的点阵字库相对

应上,我们的字库也是按这个顺序来安排的。比如,字符'a'

的数值是97,则在点阵字库中的位置是97 * 16字节处。也

就是通过ASCII的值能表明描述字符图形的16字节的初始位

置。而我们要做的是用这16字节来描画一个字符的图形。

*/

void make_ascii(int* buf, unsigned int x_size, int x, int y,

unsigned int colour, char* font)

{

int i;

int* p;

char d;

// 下面的循环是对每一个字节进行解析,共16个字节

for(i = 0; i < 16; i++) {

/*

大家对于p的计算方法可能会有疑问。p代表的是像素

在内存中地址。x、y代表的是像素的在窗口图形中的

横纵坐标,每一行的像素是窗口的横向尺寸。因此,

每换一行画点纵坐标的值要乘以该尺寸。x方向上的

8个点是用p[0]——p[7]逐个画出来的,y方向上的16行

是用循环完成的。大家可能会想,这个不就是画矩形吗?

是的,只不过通过每个条件语句决定了这个点是描绘

还是不描绘。

*/

p = buf + (y + i) * x_size + x;

d = font[i];

/*

每一个字节都有8位,这里对每一位的操作没有

使用循环,而是逐位解析逐个像素填充颜色,主要

是基于效率的考量。而且,大家直观的也能看出一个

规律,那就是每一位就是8421的循环。当然这里是

16进制的表示方法。

*/

if(d & 0x80) {p[0] = colour;}

if(d & 0x40) {p[1] = colour;}

if(d & 0x20) {p[2] = colour;}

if(d & 0x10) {p[3] = colour;}

if(d & 0x08) {p[4] = colour;}

if(d & 0x04) {p[5] = colour;}

if(d & 0x02) {p[6] = colour;}

if(d & 0x01) {p[7] = colour;}

}

}

/* 附赠的和make_ascii()功能相同的函数。虽然效率与make_ascii()

要差很多,但是更好理解了。

*/

void putfont8(int* buf, unsigned int x_size, int x, int y,

unsigned int colour, char* font)

{

int i, j;

for(j = 0; j < 16; j++) {

for(i = 0; i < 8; i++) {

if(font[j] & (0x80 >> i)) {

draw_point(buf, x_size, x + i, y + j, colour);

}

}

}

}

// 在窗口的任意位置描画字符串的的函数。

void put_str_buf(int* buf, unsigned int x_size, int x, int y,

unsigned int colour, char* s) {

// ASCII码的8 * 16点阵字符。

extern char ascii[4402];

while(*s) {

make_ascii(buf, x_size, x, y, colour, ascii + *s++ * 16);

// 字符的横向始终是占据8个像素。

x += 8;

}

}

// 我们用于在信息区持续描画字符串的函数。具有自动换行和清屏功能。

void put_str(char* s, unsigned int colour) {

extern char ascii[4402];

unsigned int* video_base = get_video_addr(multiboot2_magic,

multiboot2_addr);

// x、y分别代表横纵坐标上的像素数。

int x, y;

// 获取像素位置。

x = pos % 1024;

y = pos >> 10;

while(*s) {

/* 信息区只有1024 * 128个双字的大小,如果

纵坐标的像素数大于或等于128则清除信息区,

并且置信息去字符的起始位置位0。

*/

if(y >= 128) {

y = 0;

info_area_cls();

}

// 对于换行的处理。

if(*s == '\n') {

s++;

x = 0;

y += 16;

continue;

}

// 显示字符。

putfont8(video_base, 1024, x, y, colour, ascii + *s++ * 16);

x += 8;

/*

因为x只是坐标,所以当x大于他的极限时,

要对x进行充值,同时y坐标增加16。

*/

if(x >= 1024) {

x = 0;

y += 16;

}

}

// 每显示一次对全局像素变量进行修正。

pos = (y << 10) + x;

}

// 仅仅是在信息去显示红色的字符。

void put_str_red(char* s) {

put_str(s, 0x00ff0000);

}

// 附赠的内存复制的函数。

void memcpy_(void* d_, void* s_, unsigned int c) {

char* d = d_, * s = s_;

char* p = d_ + c;

for(;d < p;) {

*d++ = *s++;

}

}

// 附赠的内存写入特定值的函数。

void memset_(void* d_, unsigned char v, unsigned int l) {

unsigned char* d = d_;

unsigned char* s = d_ + l;

for(; d < s;) {

*d++ = v;

}

}

// 字符串拷贝函数。

char* strcpy_(char* a_, char* b_) {

char* a = a_;

while(*b_) {

*a_++= *b_++;

}

return a;

}

// 在窗口的任意位置描画中文(gb2312标准)的的函数。

void put_gb2312_buf(int* buf, int xsize, int x, int y, int colour,

unsigned char* s)

{

// 符合国标gb2312的汉字点阵字库。

extern short gb2312[267922 / 2];

unsigned char a, b;

unsigned int offset;

short* word_addr;

while(*s) {

a = s[0];

b = s[1];

// gb2312编码中汉字字符的偏移值计算方法。

offset = ((a - 0xa0 - 1) * 94 + (b - 0xa0 - 1)) * 16;

word_addr = gb2312 + offset;

putfont16(buf, xsize, x, y, colour, word_addr);

s += 2;

x += 16;

}

}

// 描画gb2312标准的汉字的函数,这个我们要在正文中详细啰嗦一下。

void putfont16(int* buf, int xsize, int x, int y, int colour, short* font) {

int i;

int* p;

short d;

for(i = 0; i < 16; i++) {

p = buf + (y + i) * xsize + x;

d = font[i];

if(d & 0x80) {p[0] = colour;}

if(d & 0x40) {p[1] = colour;}

if(d & 0x20) {p[2] = colour;}

if(d & 0x10) {p[3] = colour;}

if(d & 0x08) {p[4] = colour;}

if(d & 0x04) {p[5] = colour;}

if(d & 0x02) {p[6] = colour;}

if(d & 0x01) {p[7] = colour;}

if(d & 0x8000) {p[8] = colour;}

if(d & 0x4000) {p[9] = colour;}

if(d & 0x2000) {p[10] = colour;}

if(d & 0x1000) {p[11] = colour;}

if(d & 0x0800) {p[12] = colour;}

if(d & 0x0400) {p[13] = colour;}

if(d & 0x0200) {p[14] = colour;}

if(d & 0x0100) {p[15] = colour;}

}

}

// 求取字符串长度的函数。

unsigned int strlen_(const char* s) {

const char* t = s;

while(*t) {

t++;

}

return t - s;

}

/*

一个特殊的除法函数,商存放于放置被除数的地址中,

而返回值为本次除法的余数。

*/

unsigned int div_(unsigned int* i, unsigned int n) {

unsigned int res = *i % n;

*i /= n;

return res;

}

// 将数值转化为特定字符串,并存储于缓冲区buf中。

void i2a(char* buf, unsigned int a, unsigned int b) {

char* res = buf, * p;

if(!a) {

*res++ = '0';

*res = '\0';

return;

}

const char num[32] = "0123456789abcdef";

char t[0x100];

p = t;

// 相当于用查表的方法获取对应字符的ASCII值。

while(a) {

*p++ = num[div_(&a, b)];

}

*p = '\0';

/*

因为获得的字符串是倒置的,所以这里把字符串摆正,

怎么样教科书上做没做过类似的习题。看来书上的乏味

不是没有道理,而是我们不懂。

*/

while(p != t - 1) {

*res++ = *--p;

}

*res = '\0';

}

// 下面是一个简单的格式化字符串的函数。

int vsprintf_(char* buf, const char* format_s, char* temp) {

const char* s = format_s;

char t[0x100], * p, * res = buf;

unsigned int i;

while(*s) {

/*

当出现%时,将是变量或特殊字符的解析,

其它的按正常字符显示。

*/

if(*s == '%') {

s++;

switch(*s) {

// %%将被解释为一个%字符并显示。

case '%':

*buf++ = *s;

break;

// 预示着在可变参数中将出现一个字符。

case 'c':

i = *((char*)(temp+=4));

*buf++ = (char)i;

break;

// 在可变参数中出现一个字符指针,即字符串。

case 's':

p = *((char**)(temp+=4));

while(*p) {

*buf++ = *p++;

}

break;

// 可变参数中的数值将被转化成十进制字符串。

case 'd':

i = *((int*)(temp+=4));

i2a(t, i, 10);

p = t;

while(*p) {

*buf++ = *p++;

}

break;

// 可变参数中的数值将被转化成十六进制字符串。

case 'x':

i = *((int*)(temp+=4));

i2a(t, i, 16);

*buf++ = '0';

*buf++ = 'x';

p = t;

while(*p) {

*buf++ = *p++;

}

break;

// 可变参数中的数值将被转化成二进制字符串。

case 'b':

i = *((int*)(temp+=4));

i2a(t, i, 2);

p = t;

while(*p) {

*buf++ = *p++;

}

*buf++ = 'b';

break;

}

s++;

} else {

*buf++ = *s++;

}

}

*buf = '\0';

return strlen_(res);

}

// 实现的一个简单的printf函数,这个函数够经典吧。

int printf_(const char* format_s, ...) {

char t[0x400];

// 获得可变参数中第一个参数的地址。

char* temp = (char*)&format_s;

vsprintf_(t, format_s, temp);

temp = NULL;

put_str_red(t);

return strlen_(t);

}

上面的代码是很繁复的,幸好笔者在代码中间写了不少的注释,大家应该不会有什么疑问了。而且我们附赠了一些将来有用的函数,等以后慢慢享用。

下面是我们在kernel.c和boot.asm中添加的代码,它更加深刻地阐释了显示字符函数的原理。

【kernel.c 节选】

(上面省略)

static char font_A[16] = {

0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,

0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00

};

extern char font_A_;

make_ascii(video + 1024 * 128, 1024, 0, 0, 0x000000ff, (char*)&font_A_);

make_ascii(video + 1024 * (128 + 16), 1024, 0, 0, 0x0000ff00, font_A);

printf_("%s %d %x %b", "I love you SnailOS...!", 0x8, 8, 8);

put_gb2312_buf(video, 1024, 500, 400, 0x00aa0000, "我爱你蜗牛操作系统!");

(下面省略)

【boot.asm 节选】

(生面省略)

; 共16个字节,正好是字库中一个字符的点阵表示。

global _font_A_

_font_A_:

db 0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24

db 0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00

在上面首先我们定义了一个静态字符数组font_A,然后我们引用了一个外部的变量font_A_。这不是很奇怪吗?你猜对了吗?我们要说明的是它们在功能上是等效的,即是定义字符静态数组等价于汇编语言中db伪指令。另外要说明的是字库中图形的表示方法整整比我们最开始用的方法节省8倍的空间。原因是一个字节的每个位能够描述一个像素。

在揭开字库生成原理的大幕前,我们首先用手头的工具生成字库吧。下面是操作方法。

显示用户页

C:\Users\free2\.VirtualBox\temp\font>makefont.exe hankaku.txt

usage>makefont source.txt font.bin

转化为二进制文件

C:\Users\free2\.VirtualBox\temp\font>makefont.exe hankaku.txt ascii_font.bin

显示用户页

C:\Users\free2\.VirtualBox\temp\font>bin2obj

usage>bin2obj binfile objfile [-]label

转化为可连接的目标文件,并生成可用的全局符号。

C:\Users\free2\.VirtualBox\temp\font>bin2obj ascii_font.bin ascii_font.obj _ascii

Kankaku.txt就是一个纯文本文件,大家有兴趣可以用wps等软件打开参观一下。里面的内容就类似于我们自己定义的pic[16][16]这个数组,人可以看懂,而且能够编辑。通过它能够比较形象的看到每个字符的点阵表示形式。这当然是我们能够手工编辑的了。然后我们通过makefont.exe工具就生成了二进制的字符文件,这也就相当于我们上面的第一个循环,只不过我们用直接用像素表示比较浪费空间,毕竟每像素四字节。这里就是把字符转化为位图的表示方法,从而节省空间。接下来用bin2obj.exe工具,把纯二进制文件转化成我们能够链接的目标文件,就能够被我们的编程语言操作了。作为类比我们在boot.asm中也global调出了全局标签_font_A_为C语言程序所使用。bin2obj.exe的作用和目标也就仅此而已。并且我们知道最终生成的ascii_font.obj文件需要链接到内核文件中。

综合上面的叙述,我们不难总结一点,其实是可以纯手工的打造字库的,而且仅仅是有语言本身的就够了。不过那样不形象,以至于我们要在纸上画很多的图形,来确定字符每个字节的二进制值。所以前辈想到了,先用特殊字符编辑形象的矩形图形,在将字符图形转化成点阵字库的好方法。不过点阵字库也不过是为了节省空间,在我们使用字库的时候,还要编程把它转化的为真正像素的点才能正常的显示,这不是“卖孩子买猴”的玩法吗?也亏了这些大侠们在时空之间穿梭的能力了。

有了ASCII字符的字库,下面们该说说“简体汉字”的字库了。笔者用的是“无脑仔的小明”提供的hzk16的符合gb2312标准的字库(更详细的内容请查看“无脑仔的小明”《30天自制操作系统》实现中文显示一文)。我这只是把那些重要的东西给大家交代一下就好了。

通过我们的putfont16函数的我们可以知道,在编码中低字节存放的是汉字的左边,高字节存放的是汉字的右边,因此,如果使用两层循环(类似于画矩形)来显示汉字的话,应该第一层循环分成两部分,分别显示,不能直接从大到小地完成。而第二层循环依然是从上倒下的,所以可以一次完成。

第二个要注意的问题就是gb2312编码是怎样的得到,汉字字符的偏移的。这个标准的常用汉字是6千多不到7千个,因此,用一个字节来编码板顶是不够了。因此,制定标准的人们采用了连个字节的方式。不过不是直接用两个字节指定汉字位置的偏移,而是绕了个弯子,当然是又给我们埋了个坑。首先是把汉字分成若干个区,每个区内只放94个汉字,因此,要找汉字首先要确定区,然后是区内的位号。在两个字节的编码中低字节就是区号,高字节就是位号。同时,区号和位号也都不是随意使用,它们都从0xa1开始的,也就是在此套编码中,任何汉字的编码都应该是比0xa1a1大。编码就是这样了,偏移怎么求取呢?下面的公式告诉我们。

在点阵字符中的偏移= (94*(区码-1)+(位码-1))*32

而我们的实际与语句是offset = ((a - 0xa0 - 1) * 94 + (b - 0xa0 - 1)) * 16;

原因是我们的区位号是直接从汉字编码中得来的,所以要在此基础上减去区位的起始号0xa0,同时因为数组开始于0,所以还要在减去1。至于我们为什么乘以16,而公式乘以32,那是因为我们的数组是字类型的,而公式是字节类型的。

最后有个关键的环节还要告诉大家了,即使是你前面的事情都做得完全正确,也可能不能正常显示汉字。这又是为什么呢。实话实说,我在首次实验到这个地方的时候都快要崩溃了,最后不知从哪里才想到这个连做梦都不会想到的地方。

看到这个地方你就豁然开朗了吧。原来我们的文本编辑器默认的编码规则是ANSI。在这里也让我感叹一下吧,在如释重负之前,难道一定要经历炼丹炉、走火焰山等等等等九九八十一难吗?我不禁的这样问!

经历了前面的苦痛之后,我甚至都把毛爷爷的诗词七律《人民解放军占领南京》想起来了,就让我们在屏幕上显示出来吧!我们改写kernel.c的部分语句,同时还删除了那些实验的中间产物。这些语句还是请大家自己发挥吧,这里就不再贴出来了。

最后的最后,也就是剩下printf相关函数还没有讲解了。printf_()函数使用了c语言的一个不常用的功能——可变参数。也就是参数表中的“...”,这样一来我们就能够在一个固定的格式化字符串后使用不定的若干个参数来输出数目不等的变量信息了。

关于这个技巧的实质是一个很有价值的话题——栈与函数调用。这个问题我们已经接触过了,还记得boot.asm文件中的这几个语句吗。

push ebx

push eax

call _kernel_main

上面的三条汇编语句,就相当于c语言中的“kernel_main(eax, ebx);”,即是上面的先把ebx入栈,再把eax入栈,在调用函数(汇编中一般称为过程),这个顺序就是所谓的从右往左的顺序。至于说为什么这样,这个就叫做“王八的屁股”了。可变参数也不例外,比如咱们的printf_("%s %d %x %b", "I love you SnailOS...!", 0x8, 8, 8);语句,就是先后把8、8、0x8、"I love you SnailOS...!"和"%s %d %x %b"入栈,这其中整数的入栈当然是整数本身,字符串入栈就是字符串的首地址了。最后一个入栈的是不变参数,也就是咱们的格式化字符串。由于机器自带的硬件栈的单元长度是固定的,所以只要知道格式化字符串地址的地址,就可以推算出其后的各个可变参数的地址,有了地址就能够访问到地址中的内容。又由于栈是向下生长的,所以最后入栈的格式化字符串地址的地址就是最低地址,如果定义为字符指针的情况下,对该指针做增4的运算,就得到了下一个参数的地址,依次类推可以得到所有参数的地址。然而,大家如果明白了我上述的内容。你就会提出一个疑问,我们是怎么确定参数的个数的,如果参数的个数与格式化字符串中的不相符怎么办?遗憾的是,这类函数并无对参数相符的判断能力,全要靠我们对格式化字符串及之后参数的人工核对,也就是说必须由程序员来负责调用这类函数的正确性,即使你搞错了,因为是可变参数,c编译系统和你运行的程序都不知道参数与格式化字符串中的参数是否对应。因此,要是程序员一不留神写错了,那大概率是程序运行到此,就导致问题的发生。

还有一点是不得不说明的,刚才我们提到的kernel_main函数,因为是系统中第一个c函数,同时也是内核函数,因此他是一个有去无回的函数,从此也不会返回到调用的它的汇编代码中了。它不能称之为一次完整的函数调用。一般的,一次完整的函数调用,被调函数在执行完成后一定要返回到主调函数,call指令隐含的是操作机器栈的,通常情况下,call指令在转移到被调函数之前,会把call指令下面语句的地址压入栈,这样一来,当被调函数想要返回主调函数中继续执行时,就会从栈中得到返回地址,从而顺利返回到主调函数。ret指令正是这样的一条指令,它执行与call指令相反的操作,从栈中取回返回地址,并回到主调函数。由于call和ret分别在主调函数和被调函数中配合默契到天衣无缝的感觉,因此,我们不会特别关注它们。而由于主调函数在函数调用之前,通过操作栈类指令改变了栈指针。通常情况下,在call指令下,我们会加入恢复栈指针的指令,拿kernel_main为例,函数调用前有两个参数入栈,因此在返回后,会执行add esp, 2 * 4指令使栈指针恢复原位。这样做的目的是显而易见的,因为栈既是传送数据的缓冲区,同时又是返回地址的根据地。在多重的函数调用过程中,如果不对栈指针进行恢复,程序必定会因为返回地址的错乱而跑飞的。

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

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

相关文章

【电子学会】2022年12月图形化四级 -- 简易抗疫物资管理系统

简易抗疫物资管理系统 1. 准备工作 (1)角色:从角色库中添加4个按钮,添加文字“增加”、“删除”、“修改”、“查询”,修改角色名字为“增加按钮”、“删除按钮”、“修改按钮”、“查询按钮”; (2)列表:新建列表“抗疫物资清单”。 2. 功能实现 (1)点击“增加按…

第三章 Linux中的shell与权限

第三章 Linux中的shell与权限一、linux的内核&#xff08;kernel&#xff09;与外壳&#xff08;shell&#xff09;1、内核与外壳的关系2、外壳的作用二、权限1、用户中的权限&#xff08;1&#xff09;超级用户&#xff1a;root&#xff08;2&#xff09;普通用户a.普通用户的…

超实用的微信公众号内容运营方案分享

公众号运营的本质就是图文生产&#xff0c;内容绝对是涨粉引流的关键。没有产出好的内容&#xff0c;这个公众号是绝对走不长远的。 公众号内容运营大致上可以分为两个大方向&#xff0c;一个是搭建完整的公众号内容体系&#xff0c;一个是创作具体的公众号推文内容&#xff0…

Sklearn标准化和归一化方法汇总(2):Min-Max归一化

Sklearn中与特征缩放有关的五个函数和类&#xff0c;全部位于sklearn.preprocessing包内。作为一个系列文章&#xff0c;我们将逐一讲解Sklearn中提供的标准化和归一化方法&#xff0c;以下是本系列已发布的文章列表&#xff1a; Sklearn标准化和归一化方法汇总(1)&#xff1a…

【现代机器人学】学习笔记十:机器人控制

这节的内容主要讲述在关节空间和任务空间中的运动控制中的反馈控制&#xff0c;力控制&#xff0c;运动-力混合控制以及阻抗控制、导纳控制&#xff0c;pid控制等内容。在之前的内容当中&#xff0c;往往不涉及到实际对机器人的操纵&#xff0c;即我们计算出一个结果&#xff0…

【5】KubeSphere部署应用 | MySQL

目录 1、部署的架构 2、KubeSphere几个主要的模块 3、部署MySQL 【1】先创建MySQL的配置文件 【2】创建存储卷 【3】部署有状态服务 【4】查看创建的服务 【5】创建一个服务可以在集群外可以访问 1、部署的架构 2、KubeSphere几个主要的模块 KubeSphere的工作负载相当于k8s里…

算法之常见字符串题目

leedcode344. 反转字符串 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 示例 1&#xff1a; 输入&#xff1a;s […

进程学习笔记

进程 定义 一个程序程序在一个数据集合上的动态执行过程 与程序区别 动静&#xff0c;暂时的过程和永久的存在&#xff0c;进程由程序、数据、进程控制块&#xff08;PCB&#xff09;组成 特性 动态并发&#xff08;进程&#xff09;独立&#xff08;分页有力支持&#x…

Linux:C/C++文件操作

回忆C语言文件操作 fopen()的使用&#xff1a;   答&#xff1a;打开文件流指针。 param2 “w”&#xff0c;当前没有文件就先创建&#xff0c;再写入。 “r”&#xff0c;只读。 “b”&#xff0c;可与w、b组合。 只写的例子&#xff1a; 只读的例子&#xff1a; 依靠fg…

《Linux性能优化实战》学习笔记 Day01

学习目标 系统优化的旅程上&#xff0c;对操作系统相关的优化是绕不开的&#xff0c;主动出击&#xff0c;将零星的知识体系化。今后遇到问题&#xff0c;能够加入自己的体系树中&#xff0c;即使专栏中没有提到&#xff0c;自己也能够想办法深入。 希望在这次课程后&#xf…

黑马Redis | 基础篇

目录 一、SQL和NoSQL的区别 结构化与非结构化 关联和非关联 查询方式 事务 总结 二、Redis数据类型和命令 1、通用命令 2、数据类型 3、String类型 String的常见命令 Key结构 4、Hash类型 常见命令 5、List类型 6、Set类型 Set的常见命令 7、SortedSet类型 …

科研快报|PacBio全长扩增子测序破解蚊子肠道微生态与耐药性差异

论文题目&#xff1a;Differences in the intestinal microbiota between insecticide-resistant and -sensitive Aedes albopictus based on full-length 16S rRNA sequencing.期刊&#xff1a;Microbiologyopen影响因子&#xff1a;3.139发表时间&#xff1a;2021年1月研究背…

一个非常好用的中奖概率控制器

本文首发于微信公众号&#xff1a; 小蚂蚁教你做游戏。欢迎关注领取更多学习做游戏的原创教程资料&#xff0c;每天学点儿游戏开发知识。嗨&#xff01;大家好&#xff0c;我是小蚂蚁。今天分享一个非常好用的概率控制器&#xff0c;可以用于游戏中两种行为出现的概率控制。这个…

PHP MySQL 插入数据

使用 MySQLi 和 PDO 向 MySQL 插入数据 在创建完数据库和表后&#xff0c;我们可以向表中添加数据。 以下为一些语法规则&#xff1a; PHP 中 SQL 查询语句必须使用引号在 SQL 查询语句中的字符串值必须加引号数值的值不需要引号NULL 值不需要引号 INSERT INTO 语句通常用于…

《Linux Shell脚本攻略》学习笔记-第十三章

13.1 简介 现代Linux应用可以部署在专门的硬件、容器、虚拟机或是云端。 容器的缺点在于它以来于主机的系统内核。 虚拟机的防在于要占用大量的磁盘空间。 如果你想同时运行多个虚拟机&#xff0c;必须要有足够的内存来支撑各个虚拟机。否则&#xff0c;主机就不得不开始交换页…

SLS:基于 OTel 的移动端全链路 Trace 建设思考和实践

作者&#xff1a;高玉龙 (元泊)首先&#xff0c;我们了解一下移动端全链路 Trace 的背景&#xff1a;从移动端的视角来看&#xff0c;一个 App 产品从概念产生&#xff0c;到最终的成熟稳定&#xff0c;产品研发过程中涉及到的研发人员、工程中的代码行数、工程架构规模、产品发…

探索VGG网络与LeNet网络对精度的影响

1 问题在学习不同网络模型对实验精度的影响过程中&#xff0c;对我们的实验结果&#xff0c;各种参数数值的改变有何变化&#xff0c;有何不同。VGG-11网络与LeNet-5网络对精度和损失的影响研究。训练周期20其他参数都相同的方式来探索最终的精度。2 方法对于VGG-11网络&#x…

QTreeWidget 设置任意行背景色

设置任意某行&#xff0c;网上这类示例少&#xff0c;一般都是选中行、交替行、高亮行等设置。 比如我要将顶层节点的背景色修改一下。 方法1&#xff0c;先继承QTreeWidget,更改它的 drawBranches函数&#xff0c;在里面添加条件判断&#xff0c;然后根据需要设置颜色。 #i…

【计算机体系结构-01】指令集体系结构、微体系结构简介

1. “虚拟” to “现实” 首先可以看这张图片&#xff0c;下面的 Physics 所指的是我们的物理世界中看得见摸得到或者是客观存在的事物&#xff0c;而人类希望将自己的工作内容或者需求以某种方式映射到物理层面上&#xff0c;用物理变化带来的影响来完成人类工作内容。例如早期…

Python【r e】模块正则表达式[中]实战

正则表达式相关函数和符号用法&#xff1a;#正则表达式""".匹配任意某个字符[.]与转义字符的作用一致&#xff0c;表示匹配.,配合 &#xff0c;[.],即匹配一次或则多次. text . 或则 text ...2.从头匹配或者从左往右匹配re.match()"""import …