介绍

好吧,您已经知道内核是什么 内核_百度百科

编写操作系统的第一部分是以 16 位汇编(实模式)编写引导加载程序。
引导加载程序是在任何操作系统运行之前运行的程序。
它用于引导其他操作系统,通常每个操作系统都有一组特定的引导加载程序。
转到以下链接以在 16 位汇编中创建您自己的引导加载程序

https://createyourownos.blogspot.in/

引导加载程序通常选择一个特定的操作系统并启动它的进程,然后操作系统将自身加载到内存中。
如果您正在编写自己的引导加载程序来加载内核,您需要了解内存的整体寻址/中断以及 BIOS。
大多数情况下,每个操作系统都有特定的引导加载程序。
在线市场上有很多可用的引导加载程序。
但是有一些专有的引导加载程序,例如用于 Windows 操作系统的 Windows Boot Manager 或用于 Apple 操作系统的 BootX。
但是有很多免费和开源的引导加载程序。看比较,

https://en.wikipedia.org/wiki/Comparison_of_boot_loaders

其中最著名的是 GNU GRUB - 来自 GNU 项目的 GNU Grand Unified Bootloader 包,用于类 Unix 系统。

https://en.wikipedia.org/wiki/GNU_GRUB

我们将使用 GNU GRUB 来加载我们的内核,因为它支持许多操作系统的多重引导。

要求

GNU/Linux :-  任何发行版(Ubuntu/Debian/RedHat 等)。
Assembler :-  GNU Assembler( gas ) 用于组装汇编语言文件。
GCC :-   GNU 编译器集合,C 编译器。任何版本 4、5、6、7、8 等
Xorriso :-  创建、加载、操作 ISO 9660 文件系统映像的包。(man xorriso)
grub-mkrescue :-  制作 GRUB 救援映像,此包在内部调用 xorriso构建iso映像的功能。
QEMU :-  快速 EMUlator 在虚拟机中启动我们的内核,而无需重新启动主系统。

使用代码

好吧,从头开始编写内核就是在屏幕上打印一些东西。
所以我们有一个VGA(Visual Graphics Array),一个控制显示器的硬件系统。

https://en.wikipedia.org/wiki/Video_Graphics_Array

VGA 具有固定数量的内存,地址为0xA00000xBFFFF

0xA0000用于 EGA/VGA 图形模式 (64 KB)
0xB0000用于单色文本模式 (32 KB)
0xB8000用于彩色文本模式和 CGA 兼容图形模式 (32 KB)

首先,您需要一个指示 GRUB 加载它的多重引导引导加载程序文件。
必须定义以下字段。

Magic :-一个固定的十六进制数,由引导加载程序标识为要加载的内核的标头(起点)。
flags :-如果设置了 flags 字中的位 0,则与操作系统一起加载的所有引导模块必须在页面 (4KB) 边界上对齐。
校验和:-引导加载程序用于特殊用途,其值必须是魔术编号和标志的总和。

我们不需要其他信息, 
但更多详细信息  https://www.gnu.org/software/grub/manual/multiboot/multiboot.pdf

好的,让我们为上述信息编写一个 GAS 汇编代码。
如上图所示,我们不需要某些字段。

boot.S

# set magic number to 0x1BADB002 to identified by bootloader
.set MAGIC,    0x1BADB002# set flags to 0
.set FLAGS,    0# set the checksum
.set CHECKSUM, -(MAGIC + FLAGS)# set multiboot enabled
.section .multiboot# define type to long for each data defined as above
.long MAGIC
.long FLAGS
.long CHECKSUM# set the stack bottom
stackBottom:# define the maximum size of stack to 512 bytes
.skip 1024# set the stack top which grows from higher to lower
stackTop:.section .text
.global _start
.type _start, @function_start:# assign current stack pointer location to stackTopmov $stackTop, %esp# call the kernel main sourcecall kernel_entrycli# put system in infinite loop
hltLoop:hltjmp hltLoop.size _start, . - _start

我们定义了一个大小为 1024 字节的堆栈,并由 stackBottom 和 stackTop 标识符管理。
然后在 _start 中,我们存储一个当前堆栈指针,并调用内核的主函数(kernel_entry)。

如您所知,每个流程都由不同的部分组成,例如数据、bss、rodata 和文本。
您可以通过编译源代码而不组装它来查看每个部分。

例如:运行以下命令
        gcc -S kernel.c
      并查看 kernel.S 文件。

而这部分需要一个内存来存储它们,这个内存大小由链接器映像文件提供。
每个内存都与每个块的大小对齐。
它主要需要将所有目标文件链接在一起以形成最终的内核映像。
链接器图像文件提供了应该为每个部分分配多少大小。
信息存储在最终的内核映像中。
如果您在 hexeditor 中打开最终的内核映像(.bin 文件),您会看到很多 00 字节。
链接器映像文件包含一个入口点(在我们的例子中,它是在文件 boot.S 中定义的 _start)和在 BLOCK 关键字中定义的大小与间距大小对齐的部分。

linker.ld

/* entry point of our kernel */
ENTRY(_start)SECTIONS
{/* we need 1MB of space atleast */. = 1M;/* text section */.text BLOCK(4K) : ALIGN(4K){*(.multiboot)*(.text)}/* read only data section */.rodata BLOCK(4K) : ALIGN(4K){*(.rodata)}/* data section */.data BLOCK(4K) : ALIGN(4K){*(.data)}/* bss section */.bss BLOCK(4K) : ALIGN(4K){*(COMMON)*(.bss)}}

现在您需要一个配置文件来指示 grub 加载带有关联图像文件
grub.cfg的菜单

menuentry "MyOS" {multiboot /boot/MyOS.bin
}

现在让我们编写一个简单的 HelloWorld 内核代码。

kernel.h

#ifndef KERNEL_H
#define KERNEL_Htypedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;#define VGA_ADDRESS 0xB8000
#define BUFSIZE 2200uint16* vga_buffer;#define NULL 0enum vga_color {BLACK,BLUE,GREEN,CYAN,RED,MAGENTA,BROWN,GREY,DARK_GREY,BRIGHT_BLUE,BRIGHT_GREEN,BRIGHT_CYAN,BRIGHT_RED,BRIGHT_MAGENTA,YELLOW,WHITE,
};#endif

这里我们使用 16 位 vga 缓冲区,在我的机器上,VGA 地址从0xB8000开始,32 位从0xA0000开始。
指向 VGA 地址的无符号 16 位类型终端缓冲区指针。
它有 8*16 像素的字体大小。
见上图。

kernel.c

#include "kernel.h"/*
16 bit video buffer elements(register ax)
8 bits(ah) higher : lower 4 bits - forec olorhigher 4 bits - back color8 bits(al) lower :8 bits : ASCII character to print
*/
uint16 vga_entry(unsigned char ch, uint8 fore_color, uint8 back_color)
{uint16 ax = 0;uint8 ah = 0, al = 0;ah = back_color;ah <<= 4;ah |= fore_color;ax = ah;ax <<= 8;al = ch;ax |= al;return ax;
}//clear video buffer array
void clear_vga_buffer(uint16 **buffer, uint8 fore_color, uint8 back_color)
{uint32 i;for(i = 0; i < BUFSIZE; i++){(*buffer)[i] = vga_entry(NULL, fore_color, back_color);}
}//initialize vga buffer
void init_vga(uint8 fore_color, uint8 back_color)
{vga_buffer = (uint16*)VGA_ADDRESS;  //point vga_buffer pointer to VGA_ADDRESS clear_vga_buffer(&vga_buffer, fore_color, back_color);  //clear buffer
}void kernel_entry()
{//first init vga with fore & back colorsinit_vga(WHITE, BLACK);//assign each ASCII character to video buffer//you can change colors herevga_buffer[0] = vga_entry('H', WHITE, BLACK);vga_buffer[1] = vga_entry('e', WHITE, BLACK);vga_buffer[2] = vga_entry('l', WHITE, BLACK);vga_buffer[3] = vga_entry('l', WHITE, BLACK);vga_buffer[4] = vga_entry('o', WHITE, BLACK);vga_buffer[5] = vga_entry(' ', WHITE, BLACK);vga_buffer[6] = vga_entry('W', WHITE, BLACK);vga_buffer[7] = vga_entry('o', WHITE, BLACK);vga_buffer[8] = vga_entry('r', WHITE, BLACK);vga_buffer[9] = vga_entry('l', WHITE, BLACK);vga_buffer[10] = vga_entry('d', WHITE, BLACK);
}

vga_entry()函数返回的值是uint16类型,突出显示字符以用颜色打印它。
该值存储在缓冲区中以在屏幕上显示字符。
首先让我们将指针vga_buffer指向 VGA 地址0xB8000

Segment : 0xB800 & Offset : 0(our index variable(vga_index))
现在你有一个VGA数组,你只需要根据屏幕上打印的内容为数组的每个索引分配特定的值,就像我们通常在分配值到数组。
请参阅上面在屏幕上打印 HelloWorld 的每个字符的代码。

好的,让我们编译源代码。
在终端上键入 sh run.sh 命令。

run.sh

#assemble boot.s file
as --32 boot.s -o boot.o#compile kernel.c file
gcc -m32 -c kernel.c -o kernel.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra#linking the kernel with kernel.o and boot.o files
ld -m elf_i386 -T linker.ld kernel.o boot.o -o MyOS.bin -nostdlib#check MyOS.bin file is x86 multiboot file or not
grub-file --is-x86-multiboot MyOS.bin#building the iso file
mkdir -p isodir/boot/grub
cp MyOS.bin isodir/boot/MyOS.bin
cp grub.cfg isodir/boot/grub/grub.cfg
grub-mkrescue -o MyOS.iso isodir#run it in qemu
qemu-system-x86_64 -cdrom MyOS.iso

确保您已安装构建内核所需的所有软件包。

输出是

如您所见,将每个值分配给 VGA 缓冲区是一种开销,因此我们可以为此编写一个函数,该函数可以在屏幕上打印我们的字符串(意味着将字符串中的每个字符值分配给 VGA 缓冲区)。

kernel_2 :-

kernel.h

#ifndef KERNEL_H
#define KERNEL_Htypedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;#define VGA_ADDRESS 0xB8000
#define BUFSIZE 2200uint16* vga_buffer;#define NULL 0enum vga_color {BLACK,BLUE,GREEN,CYAN,RED,MAGENTA,BROWN,GREY,DARK_GREY,BRIGHT_BLUE,BRIGHT_GREEN,BRIGHT_CYAN,BRIGHT_RED,BRIGHT_MAGENTA,YELLOW,WHITE,
};#endif

digit_ascii_codes 是字符 0 到 9 的十六进制值。当我们想在屏幕上打印它们时需要它们。vga_index 是我们的 VGA 数组索引。为该索引分配值时 vga_index 会增加。要打印 32 位整数,首先需要将其转换为字符串,然后再打印字符串。
BUFSIZE 是我们 VGA 的极限。要打印新行,您必须根据像素字体大小跳过 VGA 指针(vga_buffer)中的一些字节。
为此,我们需要另一个变量来存储当前行索引(next_line_index)。

#include "kernel.h"//index for video buffer array
uint32 vga_index;
//counter to store new lines
static uint32 next_line_index = 1;
//fore & back color values
uint8 g_fore_color = WHITE, g_back_color = BLUE;
//digit ascii code for printing integers
int digit_ascii_codes[10] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39};/*
16 bit video buffer elements(register ax)
8 bits(ah) higher : lower 4 bits - forec olorhigher 4 bits - back color8 bits(al) lower :8 bits : ASCII character to print
*/
uint16 vga_entry(unsigned char ch, uint8 fore_color, uint8 back_color)
{uint16 ax = 0;uint8 ah = 0, al = 0;ah = back_color;ah <<= 4;ah |= fore_color;ax = ah;ax <<= 8;al = ch;ax |= al;return ax;
}//clear video buffer array
void clear_vga_buffer(uint16 **buffer, uint8 fore_color, uint8 back_color)
{uint32 i;for(i = 0; i < BUFSIZE; i++){(*buffer)[i] = vga_entry(NULL, fore_color, back_color);}next_line_index = 1;vga_index = 0;
}//initialize vga buffer
void init_vga(uint8 fore_color, uint8 back_color)
{vga_buffer = (uint16*)VGA_ADDRESS;clear_vga_buffer(&vga_buffer, fore_color, back_color);g_fore_color = fore_color;g_back_color = back_color;
}/*
increase vga_index by width of row(80)
*/
void print_new_line()
{if(next_line_index >= 55){next_line_index = 0;clear_vga_buffer(&vga_buffer, g_fore_color, g_back_color);}vga_index = 80*next_line_index;next_line_index++;
}//assign ascii character to video buffer
void print_char(char ch)
{vga_buffer[vga_index] = vga_entry(ch, g_fore_color, g_back_color);vga_index++;
}uint32 strlen(const char* str)
{uint32 length = 0;while(str[length])length++;return length;
}uint32 digit_count(int num)
{uint32 count = 0;if(num == 0)return 1;while(num > 0){count++;num = num/10;}return count;
}void itoa(int num, char *number)
{int dgcount = digit_count(num);int index = dgcount - 1;char x;if(num == 0 && dgcount == 1){number[0] = '0';number[1] = '\0';}else{while(num != 0){x = num % 10;number[index] = x + '0';index--;num = num / 10;}number[dgcount] = '\0';}
}//print string by calling print_char
void print_string(char *str)
{uint32 index = 0;while(str[index]){print_char(str[index]);index++;}
}//print int by converting it into string
//& then printing string
void print_int(int num)
{char str_num[digit_count(num)+1];itoa(num, str_num);print_string(str_num);
}void kernel_entry()
{//first init vga with fore & back colorsinit_vga(WHITE, BLACK);/*call above function to print somethinghere to change the fore & back colorassign g_fore_color & g_back_color to color valuesg_fore_color = BRIGHT_RED;*/print_string("Hello World!");print_new_line();print_int(123456789);print_new_line();print_string("Goodbye World!");}

正如您所看到的,调用每个函数来显示值是开销,这就是为什么 C 编程提供了一个带有格式说明符的printf()函数,该函数使用每个说明符使用诸如 \ 之类的文字向标准输出设备打印/设置特定值n、\t、\r 等。

键盘 :-

对于键盘 I/O,使用端口号 0x60 和输入/输出指令。从键盘下载 kernel_source 代码。它从用户那里读取击键并将它们显示在屏幕上。

#ifndef KEYBOARD_H
#define KEYBOARD_H#define KEYBOARD_PORT 0x60#define KEY_A 0x1E
#define KEY_B 0x30
#define KEY_C 0x2E
#define KEY_D 0x20
#define KEY_E 0x12
#define KEY_F 0x21
#define KEY_G 0x22
#define KEY_H 0x23
#define KEY_I 0x17
#define KEY_J 0x24
#define KEY_K 0x25
#define KEY_L 0x26
#define KEY_M 0x32
#define KEY_N 0x31
#define KEY_O 0x18
#define KEY_P 0x19
#define KEY_Q 0x10
#define KEY_R 0x13
#define KEY_S 0x1F
#define KEY_T 0x14
#define KEY_U 0x16
#define KEY_V 0x2F
#define KEY_W 0x11
#define KEY_X 0x2D
#define KEY_Y 0x15
#define KEY_Z 0x2C
#define KEY_1 0x02
#define KEY_2 0x03
#define KEY_3 0x04
#define KEY_4 0x05
#define KEY_5 0x06
#define KEY_6 0x07
#define KEY_7 0x08
#define KEY_8 0x09
#define KEY_9 0x0A
#define KEY_0 0x0B
#define KEY_MINUS 0x0C
#define KEY_EQUAL 0x0D
#define KEY_SQUARE_OPEN_BRACKET 0x1A
#define KEY_SQUARE_CLOSE_BRACKET 0x1B
#define KEY_SEMICOLON 0x27
#define KEY_BACKSLASH 0x2B
#define KEY_COMMA 0x33
#define KEY_DOT 0x34
#define KEY_FORESLHASH 0x35
#define KEY_F1 0x3B
#define KEY_F2 0x3C
#define KEY_F3 0x3D
#define KEY_F4 0x3E
#define KEY_F5 0x3F
#define KEY_F6 0x40
#define KEY_F7 0x41
#define KEY_F8 0x42
#define KEY_F9 0x43
#define KEY_F10 0x44
#define KEY_F11 0x85
#define KEY_F12 0x86
#define KEY_BACKSPACE 0x0E
#define KEY_DELETE 0x53
#define KEY_DOWN 0x50
#define KEY_END 0x4F
#define KEY_ENTER 0x1C
#define KEY_ESC 0x01
#define KEY_HOME 0x47
#define KEY_INSERT 0x52
#define KEY_KEYPAD_5 0x4C
#define KEY_KEYPAD_MUL 0x37
#define KEY_KEYPAD_Minus 0x4A
#define KEY_KEYPAD_PLUS 0x4E
#define KEY_KEYPAD_DIV 0x35
#define KEY_LEFT 0x4B
#define KEY_PAGE_DOWN 0x51
#define KEY_PAGE_UP 0x49
#define KEY_PRINT_SCREEN 0x37
#define KEY_RIGHT 0x4D
#define KEY_SPACE 0x39
#define KEY_TAB 0x0F
#define KEY_UP 0x48#endif

inb() 从指定端口接收字节并返回。

outb() 将字节发送到指定端口。

uint8 inb(uint16 port)
{uint8 ret;asm volatile("inb %1, %0" : "=a"(ret) : "d"(port));return ret;
}void outb(uint16 port, uint8 data)
{asm volatile("outb %0, %1" : "=a"(data) : "d"(port));
}char get_input_keycode()
{char ch = 0;while((ch = inb(KEYBOARD_PORT)) != 0){if(ch > 0)return ch;}return ch;
}/*
keep the cpu busy for doing nothing(nop)
so that io port will not be processed by cpu
here timer can also be used, but lets do this in looping counter
*/
void wait_for_io(uint32 timer_count)
{while(1){asm volatile("nop");timer_count--;if(timer_count <= 0)break;}
}void sleep(uint32 timer_count)
{wait_for_io(timer_count);
}void test_input()
{char ch = 0;char keycode = 0;do{keycode = get_input_keycode();if(keycode == KEY_ENTER){print_new_line();}else{ch = get_ascii_char(keycode);print_char(ch);}sleep(0x02FFFFFF);}while(ch > 0);
}void kernel_entry()
{init_vga(WHITE, BLUE);print_string("Type here, one key per second, ENTER to go to next line");print_new_line();test_input();}

每个键码都通过函数get_ascii_char()转换为其 ASCII 字符。

制图图形用户界面:-

下载 DOSBox 等旧系统中使用的绘图框的 kernel_source (kernel_source/GUI/)

井字游戏:-

我们有打印代码、键盘 I/O 处理和使用绘图字符的 GUI。所以让我们在内核中编写一个简单的井字游戏,可以在任何 PC 上运行。

载 kernel_source 代码,kernel_source/Tic-Tac-Toe。

怎么玩 :

使用箭头键(上、下、左、右)在单元格之间移动白框,然后按空格键选择该单元格。

玩家 1 的盒子为红色,玩家 2 的盒子为蓝色。

请参阅轮到哪个玩家轮到选择单元格。(轮到:玩家 1)

如果您在实际硬件上运行此程序,则增加 tic_tac_toe.c 中的 launch_game() 和 kernel.c 中的 kernel_entry() 中的 sleep() 函数的值,以便正常工作不会太快。我使用了 0x2FFFFFFF。

有关从头开始的操作系统、操作系统计算器和操作系统中的低级图形的更多信息。

源代码链接https://download.csdn.net/download/qq_20173195/86500517

参考

  • Expanded Main Page - OSDev Wiki
  • BrokenThorn Entertainment
  • MikeOS - simple x86 assembly language operating system
  • GitHub - pritamzope/OS: Writing & Making Operating System and Kernel parts so simple like Hello World Programs, Starting from writing Bootloaders, Hello World Kernel, GDT, IDT, Terminal, Keyboard/Mouse, Memory Manager, HDD ATA R/W, VGA/VESA Graphics

用C语言编写你自己内核相关推荐

  1. linux内核是用什么语言编写的?

    严格来说,绝大部分代码是用 C 语言编写的,但在某些关键地方使用了汇编代码,其中主要是在 Linux 的启动部分.由于这部分代码与硬件的关系非常密切,即使是 C 语言也会有些力不从心,而汇编语言则能够 ...

  2. Redox随笔(2)-用Rust语言编写的类UNIX操作系统

    与其他操作系统相比,Redox如何 我们与其他操作系统有很多共同之处. 由于 Redox syscall接口是Unix-y.例如,我们有open, pipe, pipe2, lseek, read, ...

  3. Redox随笔(1)-用Rust语言编写的类UNIX操作系统

    Redox是一个用Rust语言编写的类UNIX操作系统 , 它的目标是把Rust语言的创新带入到一个现代的微内核和全系列的应用程序. https://www.redox-os.org/zh/docs/ ...

  4. java 内核驱动程序_内核第三讲,进入ring0,以及编写第一个内核驱动程序.

    内核第三讲,进入ring0,以及编写第一个内核驱动程序. 一丶进入ring0之前的简介 进入0环之前,我们要明白操作系统的设计,操作系统允许驱动程序使用In out等等特权指令来操作高2G的内存.那么 ...

  5. swift android界面,使用 Swift 语言编写 Android 应用入门

    原标题:使用 Swift 语言编写 Android 应用入门 Swift标准库可以编译安卓armv7的内核,这使得可以在安卓移动设备上执行Swift语句代码.本文解释了如何在你的安卓手机上运行一个简单 ...

  6. fceux源码解析_FCEUX金手指加强版 - 使用Lua脚本语言编写FC/NES金手指脚本

    一直觉得大部分的FC/NES模拟器的作弊码金手指不是那么方便使用, 比如魂斗罗1代, 玩家的武器可以通过修改0xAA的值来改变: 0x11为M弹(重机枪),0x12为F弹(圈圈),0x13为S弹(散弹 ...

  7. 红旗linux9支持软件,红旗linux系统下载|红旗Linux操作系统9.0正式版下载(c语言编写) 最新版_数码资源网...

    今天带来的红旗Linux操作系统9.0正式版相信是很多从事编程行业人员非常了解的,红旗Linux系统下载是非常专业的c语言编写软件,同时红旗Linux操作系统9.0正式版还拥有开关机加速.Firstc ...

  8. [转]用 C 语言编写一个网络蜘蛛

    用 C 语言编写一个网络蜘蛛来搜索网上出现的电子邮件地址 作者:zhoulifa 来源:http://bbs.chinaunix.net/viewthread.php?tid=821361 可能大家经 ...

  9. 用C语言编写万年历6,C语言编写万年历

    <C语言编写万年历>由会员分享,可在线阅读,更多相关<C语言编写万年历(8页珍藏版)>请在人人文库网上搜索. 1.C语言编写万年历 [要求]:1 程序运行后,首先在屏幕上显示主 ...

最新文章

  1. SAP财务管控 财务总监背后的“管理大师” PDF下载
  2. 我的计算机怎么打不开怎么办理,我的电脑打不开,怎么办【解决方法】
  3. 天然气表怎么看多少方_上海考大学难度怎么样?看2019上海高考“成绩分布表”和“本科分数线”就知道了!...
  4. 词法分析 有穷自动机
  5. linux查看usb设备名称,Linux系统下查看USB设备名及使用USB设备
  6. Scratch2exe-ch将sb2文件转换为exe文件
  7. 跨境电子商务营销策略分析以速卖通为例
  8. 【C语言】%e,用科学计数法输出
  9. 干货| 364套各类风格毕业设计答辩PPT模板~
  10. 0基础跟着黑马程序员学微信小程序前端开发Day02(自学笔记)
  11. STM32接电机驱动,杜邦线供电,然后反烧问题
  12. 一维数组的定义以及使用
  13. openlayers在线地图:高德地图、天地图、谷歌、geoq(智图)
  14. 有了AI,程序猿再也不用担心有Bug了
  15. Java基础篇——面向对象编程
  16. Jmeter wrk ab压测软件对比
  17. 二进制与十进制间的转化
  18. vsomeip源码梳理 -- Event订阅流程
  19. 微软 2020 财年营收突破 1 万亿人民币、净利润 3099 亿元
  20. 使用迅雷代替SDK Manager加速下载Android SDK

热门文章

  1. matlab轴向柱塞泵动力学仿真,基于虚拟样机的轴向柱塞泵柱塞副性能研究
  2. 驱动程序开发:基于EC20 4G模块自动拨号联网的两种方式(GobiNet工具拨号和PPP工具拨号)
  3. 远程面试之企业招聘新方式
  4. Bitmap的创建使用( 一)
  5. scanf(),getchar(),gets()进一步理解
  6. 探索Mailgun:面向开发人员的电子邮件引擎
  7. Opencv学习之:使用 opencv 将图片按照指定的帧率合成视频
  8. 互联网图像中的像素级语义识别
  9. 如何使用pdpipe与Pandas构建管道?
  10. SpringBoot-心跳机制+redis实现网站实时在线人数统计