MIT CS6.828-Lab 1: Booting a PC

分享

Lab 1: Booting a PC

基础概念

处理器架构

  • i386 i686 x86是同一套指令集,86指这一系列CPUx则表示i3/i6是不同的处理器型号,x86即泛指i386 i686等,都是32位处理器
  • x86_64则为x86的衍生,为x8664位版本,另外amd64也是一样的东西
  • aarch32armv7处理器架构下的指令集
  • aarch64armv8处理器架构下的指令集

AT&T格式汇编指令介绍

  • 基本格式command source, destinationAT&T将源操作数放在第一位,目标操作数放在第二位,这与Intel格式的汇编语法是相反的
  • 在寄存器操作数前用%标记,立即操作数前用$标记
  • testb/testw/testl s d
    • test表示对两个操作数进行AND操作,其结果如果为0则将CPU中的ZF标志寄存器设置为1否则清除ZF寄存器的值,即设置为0
    • b/w/l为助记符,b用于8位字节值,w用于16位字值,l则用于32位长字值
  • je/jz 检查ZF标志是否为1, 为1则跳转到目标地址
  • jne/jnz 检查ZF标志是否为0, 为0则跳转到目标地址

ZF标志寄存器(FLAGS register)

  • 此寄存器存储上一条指令的计算结果,结果为0则此寄存器值为1,否则为0

Parts

制作内核

根据给出的 make命令构建出 kernel.img文件,这步基本没什么问题,该安装的环境装上即可,GNU的那套东西

使用QEMU加载内核

有了内核文件当然要加载,由于以后的开发不可都是真机去调试,所以介绍了使用 QEMU模拟加载内核,与上个步骤一样,该安装的装上,我是在Ubuntu 20.04 LTS环境下直接安装的QEMU,没有按照Lab步骤自己编译。使用 qemu-system-i386 kernel.imgqemu-system-i686qemu-system-x86)便能将内核跑起来,展示出一个简单的 Shell环境,可以运行两个简单的命令,help和kerninfo

常规PC内存地址布局

+------------------+  <- 0xFFFFFFFF (4GB)
|      32-bit      |
|  memory mapped   |
|     devices      |
|                  |
/\/\/\/\/\/\/\/\/\/\

/\/\/\/\/\/\/\/\/\/\
|                  |
|      Unused      |
|                  |
+------------------+  <- depends on amount of RAM
|                  |
|                  |
| Extended Memory  |
|                  |
|                  |
+------------------+  <- 0x00100000 (1MB)
|     BIOS ROM     |
+------------------+  <- 0x000F0000 (960KB)
|  16-bit devices, |
|  expansion ROMs  |
+------------------+  <- 0x000C0000 (768KB)
|   VGA Display    |
+------------------+  <- 0x000A0000 (640KB)
|                  |
|    Low Memory    |
|                  |
+------------------+  <- 0x00000000

由此图可看出,前1MB的内存空间是保留的,给各种硬件设备使用,例如从0x000F0000->0x000FFFFFBIOS的地址空间(想法:用来存储BIOS程序的地址空间?),从0x000A0000->0x000BFFFFVGA显示的内存空间,而前1MB以外的所有内存叫做拓展内存,前640KB则叫做低内存区域。

  • 早期的CPU仅仅支持1MB的寻址空间,所以设计师将最顶部的区域留给了硬件,最底部的640KB留给软件使用。而当CPU终于支持了超过1MB寻址空间达到4GB后,设计师为了兼容性依然保留了1MB情况下的内存设计。
  • CPU支持4GB内存寻址(32位可寻址空间)能力后,一般会将顶部(最高位)的一些内存地址留给32位PCI设备
  • CPU支持大于4GB内存寻址能力后,也会保留32位可寻址空间顶部的一些地址空间给需要映射32位地址的设备使用
  • BIOS程序会做一些硬件自检与检测,所以会读取上述保留的硬件内存地址

使用GDB调试内核

新版的QEMU使用-s -S参数来侦听GDB的传入连接(GDB usage),同时开启另一个终端,使用make gdb命令启动GDB调试,JOS源码中已经自带GDB的相关配置(.gdbinit),跑起来后我们能看到GDB吐出的调试信息:

gdb -n -x .gdbinit
GNU gdb (Ubuntu 9.1-0ubuntu1) 9.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
+ target remote localhost:1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
The target architecture is assumed to be i8086
[f000:fff0]    0xffff0:	ljmp   $0x3630,$0xf000e05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB.  Attempting to continue with the default i8086 settings.

(gdb) 

能看到几个关键信息:[f000:fff0] 0xffff0: ljmp $0x3630,$0xf000e05b

  • [f000:fff0] 表示CPU起始的CSIP寄存器指向的地址(这两寄存器的地址加在一起组成一个完整的地址,也就是PC的概念)0xffff0,此地址指向为BIOS ROM保留的64KB区域的最顶部
  • 0xffff0地址存储了第一个BIOS程序指令:ljmp $ 0xf000,$ 0xe05b,一个jmp指令,指示跳转到CS=0x3630,IP=0xf000e05b

物理地址=16*分段地址+偏移量

16 * 0xf000(CS) + 0xfff0(IP)   # 十六进制*16只需要在后面加上0即可
= 0xf0000 + 0xfff0
= 0xffff0 

由上得知,CPU通电后CS:IP中存储的第一个地址就是BIOS ROM所在内存区域的顶部地址,这是在CPUCS:IP中写死的地址,是Intel 8086处理器开始的方式。另外需要注意的是,此处BIOS ROM所对应的实际物理位置,是在主板上的,并不是在内存条中,包括上述的一些为硬件保留的地址,也应当都是在主板上的,而非内存条提供的内存。

Boot.S文件分析

整段代码内容就是将CPU从一开始的实模式切换到保护模式,实模式下只能操作16位内存地址空间(1MB),保护模式可以启用32位地址空间(4GB)。最后设置栈指针为0x7c00,这是一个固定大小的栈,栈地址向低位地址递减。

#include <inc/mmu.h>

# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
# 
# BIOS将硬盘第一个扇区数据读取到物理内存地址0x7c00(cs=0 ip=7c00)上并使用实模式执行
# 分段地址就是为了在CPU只有16位寄存器时通过两个寄存器(CS<<4+IP)存一个物理地址的骚操作,因为当年的CPU地址线具有20根,也就是20位寄存器才能存储下,

# 定义几个常量后续会用到
.set PROT_MODE_CSEG, 0x8         # kernel code segment selector
.set PROT_MODE_DSEG, 0x10        # kernel data segment selector
.set CR0_PE_ON,      0x1         # protected mode enable flag

# 暴露出start方法给外部调用,链接器链接时需要用到
.globl start
# 开始标记
start:
  # 以16位模式运行
  .code16                     # Assemble for 16-bit mode
  # 禁用中断
  cli                         # Disable interrupts
  # 清除顺序(设置字符串操作为递增)
  cld                         # String operations increment

  # Set up the important data segment registers (DS, ES, SS).
  # 其实就是将这几个寄存器清零
  # xor %ax,%ax,一个异或操作,自己异或自己=0
  xorw    %ax,%ax             # Segment number zero
  # 将寄存器ax的值移动到ds寄存器,以下同理
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

  # Enable A20:
  #   For backwards compatibility with the earliest PCs, physical
  #   address line 20 is tied low, so that addresses higher than
  #   1MB wrap around to zero by default.  This code undoes this.
  # 遥想当年的CPU寄存器只有16位,而CPU的地址线则有20根,
  # 16位寄存器无法表示完整的20根地址线的寻址空间(1MB),于是有了分段地址这种操作。
  # 将一个寄存器的地址作为分段地址,另一个寄存器作为OFFSET,
  # 合并到一起用来表示20根地址线的寻址空间(1MB),表示方式如下:
  # 分段地址左移4位+OFFSET,换算成二进制能得到一个20位的地址,超过20位,则CPU地址线不够,
  # 所以会将第21个bit(bit位是从0开始数的,第20位其实是第21个bit位)始终设置为0
  # 因此,这里需要启用A20
  # 怎么激活呢,由于历史原因A20地址位由键盘控制器芯片8042管理。所以要给8042发命令激活A20
  # 8042有两个IO端口:0x60和0x64
  # 激活流程位: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60
  # 发送命令给端口前先判断端口缓冲区是否为空
seta20.1:
  # in操作是将目标地址0x64端口数据读取到al寄存器
  inb     $0x64,%al               # Wait for not busy
  # 判断al寄存器值第二位是否为1 test指令用来将 $0x2 & $al,并将结果设置给ZF标志寄存器
  testb   $0x2,%al
  # 判断ZF的值,如果缓冲区被占用跳转到seta20.1重新检测
  jnz     seta20.1

  # 将0xd1值移动到寄存器al,由于in与out操作都只能从寄存器进行操作,所以这里先移动数据到寄存器
  movb    $0xd1,%al               # 0xd1 -> port 0x64
  # 发送寄存器中的数据到端口
  outb    %al,$0x64

# 同上seta20.1
seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> port 0x60
  outb    %al,$0x60

  # Switch from real to protected mode, using a bootstrap GDT
  # and segment translation that makes virtual addresses 
  # identical to their physical addresses, so that the 
  # effective memory map does not change during the switch.
  lgdt    gdtdesc
  # cr0寄存器中的第0位用来设置是否为保护模式(Protedted Enable),设置为1表示启动保护模式
  # 先移动cr0寄存器中的数据到eax寄存器
  movl    %cr0, %eax
  # 做一个或操作,CR0_PE_ON为我们一开始定义的常量,值就是1,这个操作就是将寄存器eax中的数据第0位设置为1
  orl     $CR0_PE_ON, %eax
  # 将eax寄存器数据移动到cr0寄存器,至此已经开启保护模式
  movl    %eax, %cr0
  
  # Jump to next instruction, but in 32-bit code segment.
  # Switches processor into 32-bit mode.
  ljmp    $PROT_MODE_CSEG, $protcseg
  # 使用32位模式
  .code32                     # Assemble for 32-bit mode
protcseg:
  # Set up the protected-mode data segment registers
  movw    $PROT_MODE_DSEG, %ax    # Our data segment selector
  movw    %ax, %ds                # -> DS: Data Segment
  movw    %ax, %es                # -> ES: Extra Segment
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS
  movw    %ax, %ss                # -> SS: Stack Segment
  
  # Set up the stack pointer and call into C.
  # 设置栈指针为0x7c00
  movl    $start, %esp
  # 调用内核
  call bootmain

  # If bootmain returns (it shouldn't), loop.
  # 理论上call kernel代码后永远不会退出,但是如果真的退出,这里进行了一个循环
spin:
  jmp spin

# Bootstrap GDT
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULL				# null seg
  SEG(STA_X|STA_R, 0x0, 0xffffffff)	# code seg
  SEG(STA_W, 0x0, 0xffffffff)	        # data seg

gdtdesc:
  .word   0x17                            # sizeof(gdt) - 1
  .long   gdt                             # address gdt

至此我们已经设置CPU为保护模式并且能够寻址32位地址空间,接下来跳转到bootmain方法。

main.c文件分析

此文件主要用来引导内核程序,它将扫描硬盘第一个扇区,然后将ELF格式的kernel程序加载到内存中,然后跳转到内核入口处。

#include <inc/x86.h>
#include <inc/elf.h>

/**********************************************************************
 * This a dirt simple boot loader, whose sole job is to boot
 * an ELF kernel image from the first IDE hard disk.
 *
 * DISK LAYOUT
 *  * This program(boot.S and main.c) is the bootloader.  It should
 *    be stored in the first sector of the disk.
 *
 *  * The 2nd sector onward holds the kernel image.
 *
 *  * The kernel image must be in ELF format.
 *
 * BOOT UP STEPS
 *  * when the CPU boots it loads the BIOS into memory and executes it
 *
 *  * the BIOS intializes devices, sets of the interrupt routines, and
 *    reads the first sector of the boot device(e.g., hard-drive)
 *    into memory and jumps to it.
 *
 *  * Assuming this boot loader is stored in the first sector of the
 *    hard-drive, this code takes over...
 *
 *  * control starts in boot.S -- which sets up protected mode,
 *    and a stack so C code then run, then calls bootmain()
 *
 *  * bootmain() in this file takes over, reads in the kernel and jumps to it.
 **********************************************************************/

//定义扇区大小
#define SECTSIZE	512
//定义ELF头文件在内存中的位置
#define ELFHDR		((struct Elf *) 0x10000) // scratch space

void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);

void
bootmain(void)
{
	//定义program header 结构指针,这两个指针将指向内存中segment开始和结尾的地址
	struct Proghdr *ph, *eph;

	//将第一页加载到ELFHDR内存空间处
	readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

	//检测刚加载到ELFHDR处的数据是否是ELFHDR
	if (ELFHDR->e_magic != ELF_MAGIC)
		goto bad;

	//指定内存中的program segment起始地址为ELFHDR+ELFHDR->e_phoff就是从ELFHDR结束开始e_phoff为ELFHD结束位置的偏移量
	ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
	//指定内存中最后一个program segment地址
	eph = ph + ELFHDR->e_phnum;
	//循环将扇区的program segment加载到内存中
	for (; ph < eph; ph++)
		//指针++则表示指针指向的地址+指针数据结构大小*2,这里可以看成指针指向下一个program segment
		//ph->p_pa为需要加载到内存中地址的起始位置,ph->p_memsz为需要的内存大小,ph->p_offset为program segment
		//数据所在硬盘上的基于ELFHDR起始地址的偏移量
		readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

	//跳转到内核入口处(示例中ELF入口指定为0x10000c处,内核代码被加载到由ELF指定的LMA:0x100000处)
	((void (*)(void)) (ELFHDR->e_entry))();

bad:
	outw(0x8A00, 0x8A00);
	outw(0x8A00, 0x8E00);
	while (1)
		/* do nothing */;
}

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
	//program segment在内存中的结束地址
	uint32_t end_pa;
	//由起始地址+内存大小
	end_pa = pa + count;

	pa &= ~(SECTSIZE - 1);

	//将偏移量转换为扇区
	offset = (offset / SECTSIZE) + 1;

	// If this is too slow, we could read lots of sectors at a time.
	// We'd write more to memory than asked, but it doesn't matter --
	// we load in increasing order.
	//循环读取
	while (pa < end_pa) {
		// 将一个扇区读取到指定的内存位置
		readsect((uint8_t*) pa, offset);
		pa += SECTSIZE;
		offset++;
	}
}

void
waitdisk(void)
{
	// wait for disk reaady
	while ((inb(0x1F7) & 0xC0) != 0x40)
		/* do nothing */;
}

void
readsect(void *dst, uint32_t offset)
{
	// 等待硬盘准备好
	waitdisk();
	// 设置读取扇区的数目为1
	outb(0x1F2, 1);		// count = 1
	outb(0x1F3, offset);
	outb(0x1F4, offset >> 8);
	outb(0x1F5, offset >> 16);
	outb(0x1F6, (offset >> 24) | 0xE0);
	outb(0x1F7, 0x20);	// 0x20命令,读取扇区
	
	//上面四条指令联合制定了扇区号
	//在这4个字节线联合构成的32位参数中
	//29-31位强制设为1
	//28位(=0)表示访问"Disk 0"
	//0-27位是28位的偏移量

	waitdisk();

	// 读取数据到目标内存位置,每次读取128 bit读取4次
	insl(0x1F0, dst, SECTSIZE/4);
}

内核加载顺序

BIOS 程序硬件检测完成后会将硬盘引导扇区加载到0x7c00至0x7dff的内存地址中,然后设置PC为0x7c00执行boot.S

boot.S将CPU从real mode转换为protected mode,这将启用32位寻址空间,然后设置栈指针为0x7c00,栈空间增加时,栈指针地址递减,所以这将是一个固定大小的栈空间。设置完栈指针后,调用main.c

main.c扫描硬盘的第一页,加载ELF格式的文件头,根据ELF的文件定义将内核加载到指定内存地址(0x00100000),然后跳转到内核程序入口(0x0010000c)开始执行entry.S

entry.S之前使用的都为物理地址,entry.S则将entrypgdir.c中定义的页目录加载到cr3寄存器,这会将物理地址前4MB(0x000000-0x3ff000)映射到虚拟地址KERNBASE+4MB的位置。然后设置cr0寄存器以开启分页,最后设置%esp指定栈指针位置后跳转到i386_init.c

Booting Output

Booting from Hard Disk..
6828 decimal is 15254 octal!
entering test_backtrace 5
entering test_backtrace 4
entering test_backtrace 3
entering test_backtrace 2
entering test_backtrace 1
entering test_backtrace 0
Stack backtrace:
  ebp f0110f18  eip f01000a5  args 00000000 00000000 00000000 f010004e f0112308
    kern/init.c:18: test_backtrace+101
  ebp f0110f38  eip f010007a  args 00000000 00000001 f0110f78 f010004e f0112308
    kern/init.c:16: test_backtrace+58
  ebp f0110f58  eip f010007a  args 00000001 00000002 f0110f98 f010004e f0112308
    kern/init.c:16: test_backtrace+58
  ebp f0110f78  eip f010007a  args 00000002 00000003 f0110fb8 f010004e f0112308
    kern/init.c:16: test_backtrace+58
  ebp f0110f98  eip f010007a  args 00000003 00000004 00000000 f010004e f0112308
    kern/init.c:16: test_backtrace+58
  ebp f0110fb8  eip f010007a  args 00000004 00000005 00000000 f010004e f0112308
    kern/init.c:16: test_backtrace+58
  ebp f0110fd8  eip f0100110  args 00000005 00001aac 00000640 00000000 00000000
    kern/init.c:41: i386_init+102
  ebp f0110ff8  eip f010003e  args 00000003 00001003 00002003 00003003 00004003
    debuginfo_eip error
leaving test_backtrace 0
leaving test_backtrace 1
leaving test_backtrace 2
leaving test_backtrace 3
leaving test_backtrace 4
leaving test_backtrace 5
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K>

Make Grade Output

$ make grade
make clean
make[1]: 进入目录“/home/fattiger/work/MIT-CS6.828”
rm -rf obj .gdbinit jos.in qemu.log
make[1]: 离开目录“/home/fattiger/work/MIT-CS6.828”
./grade-lab1
make[1]: 进入目录“/home/fattiger/work/MIT-CS6.828”
+ as kern/entry.S
+ cc kern/entrypgdir.c
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/printf.c
+ cc kern/kdebug.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
ld: warning: section `.bss' type changed to PROGBITS
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 412 bytes (max 510)
+ mk obj/kern/kernel.img
make[1]: 离开目录“/home/fattiger/work/MIT-CS6.828”
running JOS: (1.1s)
  printf: OK
  backtrace count: OK
  backtrace arguments: OK
  backtrace symbols: OK
  backtrace lines: OK
Score: 50/50

阅读更多

以太坊黑暗森林-抢跑(front running)

以太坊黑暗森林-抢跑(front running)

前言 鸽了很久之后的今天突然心血来潮,准备写一个系列:以太坊黑暗森林,它介绍以太坊生态上的各种奇思妙想和逆天的攻击方式,会从简单的、常见的攻击方式开始介绍。取这个名字是因为我接触以太坊不久后看的一篇文章 Ethereum is a Dark Forest ,让我想起了《三体》小说中刘慈欣描述的黑暗森林,以太坊是一个弱肉强食的、没有规则的世界,猎人们总是躲在背后监听所有的交易,一旦发现猎物,它们会把它的血给吸干。 开盘抢币 相信进入以太坊生态的韭菜们,一定有过在 uniswap 上买刚开盘新币的经历,新开盘的币,一般会上涨几倍甚至十几倍,越早买入则越能低价买入。你守着时间,等着项目方添加流动性后第一时间买入代币,但是你发现,无论你的手速多块,总是看到一开盘,价格已经飚了几倍,你骂骂咧咧,开始不断拉高 gas 费用,尝试继续买入,但是你眼睁睁的看着代币涨到十倍,自己的交易却一直失败,你开始怀疑项目方自己抢跑,怀疑项目方捣鬼:肯定是项目方吃相难看,用老鼠仓提前买了。另一些聪明人,研究了以太坊的基本技术,他们在 ethscan

By FatTiger
C#:IDisposable 和 析构函数

C#:IDisposable 和 析构函数

C# 中有两种释放资源的方式:实现 IDisposable 或使用析构函数。通常,必须在特定时间释放资源的场景中,我们实现 IDisposable,像这样: public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } public void Dispose() { if (disposed) { return; } disposed = t

By FatTiger
ThreadLocal引发的灾难

ThreadLocal引发的灾难

在 Java 里有个称之为线程本地变量的类型叫做 ThreadLocal,它与 ThreadLocal 之于 C# 中是一样的作用,可以在线程范围内设置变量,这个变量只会在当前线程可被访问,但是它们有一点不同的是,在 Java 中,当你设置好变量后,在线程使用完毕回到线程池之前,需要手动调用 ThreadLocal.remove() 方法去清除线程本地变量,否则变量随着线程回到线程池,并且在下次使用此线程时此变量继续存在,而在 C# 中,线程回到线程池时会自动清除本地变量,因此无需手动去清除。 我们的业务有这样一个场景:某个业务 UserService 类中,具有多个方法会频繁(甚至循环)调用一个获取用户标签的接口,具体原因是因为某些方法会进行递归,数据结构有个树状结构,因此,为了优化接口响应时间以及看起来不那么蠢,我使用 ThreadLocal 将用户标签接口的返回数据存储到当前线程,因为在单个请求中,多次调用此接口获取数据是不必要的,它看起来像这样: /** * 此静态变量ThreadLocal会为每个线程创建本地副本, 因此USER_TAGS_THREAD_

By FatTiger
我在币安智能链的日子-区块链基础

我在币安智能链的日子-区块链基础

区块和链 无论是比特币还是以太坊,都是具有一个个区块(称之为Block)的链式结构,学过<数据结构>的肯定明白链表,区块链就像一个链表,每个区块都存储上一个区块哈希。 链(称之为Chain),有非常多的链,他们的协议不同,技术也不尽相同,比特币网络是一个链,以太坊网络是另一个链,每个链都有自己的目标(甚至目标只是为了圈钱),每个链也都有自己的代币,比特币网络的代币是比特币,每次交易都需要比特币作为手续费,以太坊网络代币是以太币,每次在以太坊网络的交易都需要以太币作为手续费。所以,链实际上作为基础设施,非常多的团队喜欢创建新的链,但是一个链光有网络光有代币不行,没有生态,很难成功。 币安智能链(Binance Smart Chain:BSC) 我的主要操作都是在BSC上,没有其它原因,只因为一个穷字。在BTC网络交易,需要BTC用作手续费,这个我可用不起,在以太坊(Ethereum)网络交易,需要以太币(ETH)作为用作手续费,按照以太币目前(

By FatTiger