跳到主要内容

有关电话和电话号码

· 阅读需 24 分钟
wjc133
Back-end Engineer @ SHOPLINE

工作原因经常需要处理手机号,一般都是用一些开源库来解析。最近看了一下开源库的实现,发现对电话号码的定义还挺复杂的。所以结合之前了解到的一些 PSTN 的知识对电话和电话号码相关的内容进行了整理。

Plan9 & Golang

· 阅读需 13 分钟
Agility6
Back-end Engineer @ SHOPLINE

在上篇文章说到了学习Plan 9基础可以为我们揭开底层的一些细节,从而通过实践去探究原理。接下来就以Plan 9为基础,从不同角度去探索Golang语言吧。

如果你还没有阅读Plan 9相关的知识,推荐阅读Plan9 & Go Assembler

环境说明:Mac m1 (ARM架构)、Golang v1.23.2

简单回顾

这里简单回顾几个比较重要的知识点吧

  • Plan 9汇编伪寄存器

    • SB(Static base pointer)用于访问全局符号,比如函数、全局变量
    • FP(Frame pointer)用于访问函数的参数和返回值
    • PC(Program counter)保存CPU下一条要运行的指令
    • SP(Stack pointer)指向当前栈帧的栈顶
  • Plan 9源操作数与目标操作数方向,源操作数在前,目的操作数在后

    • movl $0x2, %eax 将立即数0x2移动到eax寄存器

再次窥探函数

函数序言

在函数调用的时候,会经常看到这样一段的函数序言,主要的作用就是保存「调用者的」BP

pushq     %rbp
movq %rsp, %rbp
subq %16, %rsp

大致步骤如下

  1. 初始状态:函数尚未被调用

此时函数只包含返回地址(即调用函数的下一条指令的地址)

image

  1. pushq %rbp

将调用者的栈帧指针(%rbp)压入栈 保存上一个栈帧的基地址,用于函数返回时恢复调用者的栈帧

image

  1. moveq %rsp, %rbp

将当前栈指针%rsp的值赋给%rbp, 建立当前函数的栈帧基地址 标记当前函数的栈帧起点

image

  1. subq $16, %rsp

    将栈指针%rsp向下移动16字节,为局部变量分配空间 完成局部变量栈空间的分配

    image

下面就来编写代码,验证一下吧

go tool compile -S -N -l add_func.go

// base/prologue/add_func.go
package main

func add(a, b int) int {
return a + b
}

func main() {
_ = add(1, 2)
}

  • MOVD.W  R30, -32(RSP) 保存调用者的链接寄存器(R30)到栈中。
  • MOVD    R29, -8(RSP) 保存当前帧指针(R29)到栈中。
  • SUB     $8, RSP, R29 更新栈帧指针R29。
0x0000 00000         TEXT    main.add(SB), NOSPLIT|LEAF|ABIInternal, $32-16  
0x0000 00000 MOVD.W R30, -32(RSP)
0x0004 00004 MOVD R29, -8(RSP)
0x0008 00008 SUB $8, RSP, R29

再来看看函数尾声的分析

ADD $24, RSP, R29 恢复栈帧指针

  • ADD $32, RSP 释放栈空间
  • RET (R30) 返回调用者。
0x0024 00036         ADD     $24, RSP, R29
0x0028 00040 ADD $32, RSP
0x002c 00044 RET (R30)

注意架构差异,Arm架构中使用的是MOVD.W保存寄存器到栈上

调用规约

函数的调用规约用于规定如何在程序中调用函数,例如参数的传递方式、返回值的处理和寄存器的使用等x86 calling conventions

在这里只需要明白,Go在1.17之后使用了基于寄存器的调用规约。当然寄存器不是无限使用的,当达到一定程度就会使用栈传递。

image

不同的架构对于寄存器的使用可能会不一样~

Plan9 & 基础数据结构

string

创建一个简单的字符串,提取出关键的汇编代码var a = "hello"

// data/string/main.go
package main

func main() {
var a = "hello"
println(a)
}

  • "hello"字符串加载到寄存器R0

  • 随后将立即数5,也就是我们程序字符串的长度加载到寄存器R0

  • R0, main.a-16(SP)R0, main.a-8(SP)也就是它们相隔的位置,如图所示

    image

	0x0018 00024 	MOVD	$go:string."hello"(SB), R0 # 这里仅仅是地址
0x0020 00032 MOVD R0, main.a-16(SP)
0x0024 00036 MOVD $5, R0
0x0028 00040 MOVD R0, main.a-8(SP)

通过汇编也可以看到Go中的字符串结构是很简单的

type stringStruct struct {
str unsafe.Pointer
len int
}

stirng作为参数传递

如果问你在Go中字符串是如何进行参数传递的呢?这个时候只需要写一个简单的测试done,关注重点的汇编部分就可以解答了

// data/string/string_param.go
package main

func foo(str string) {
println("foo => " + str)
str = "hello-golang"
println("foo => " + str)
}

func main() {
var a = "hello"
println("main => " + a)
foo(a)
println("main => " + a)
}

十分清楚的可以看到,将main.a-48(SP)main.a-40(SP)(就是前面分析的字符串的值和长度)分别拷贝到R0R1寄存器中;最后调用foo函数。

因此可以说明**string作为参数传递是会拷贝一份的,但是注意底层数组是不是拷贝的**

	0x0064 00100 	MOVD	main.a-48(SP), R0
0x0068 00104 MOVD main.a-40(SP), R1
0x006c 00108 CALL main.foo(SB)

Array

日常在Golang开发中Array使用的其实并不多,更多作为一个底层的实现。array的结构是不需要长度这个字段的,因为它是定长的,也就是说它在编译期就能够确定长度是一个连续的内存区域。

那么就来验证一下是不是真的如上所说

// data/array/main.go
package main

func main() {
array := [5]int{10, 1, 2, 4, 5}
_ = array
}

  • LDP:是 ARM 架构的 “Load Pair” 指令,表示加载两个连续的值,到寄存器对 (R0, R1) 中。
  • 总结一下这里的操作都是在获取值并且存入到寄存器中,并没有向string初始化到时候看到有关长度的信息
	0x000c 00012 	LDP	main..stmp_0(SB), (R0, R1)
0x0018 00024 PCDATA $0, $-4
0x0018 00024 LDP main..stmp_0+16(SB), (R2, R3)
0x0024 00036 PCDATA $0, $-1
0x0024 00036 STP (R0, R1), main.array-40(SP)
0x0028 00040 STP (R2, R3), main.array-24(SP)
0x002c 00044 MOVD $5, R0
0x0030 00048 MOVD R0, main.array-8(SP)

默认值

相信你一定知道,在定义array的时候如果是没有进行初始化,那么默认值就是0,在汇编层面上处理默认值,也是会根据不同的定长,选择对应的方法。

array := [2]int // 在汇编中默认直接使用STP	(ZR, ZR), main.array-16(SP)进行0值的初始化

array1 := [10]int // 使用runtime.duffzero进行初始化

duffzero也是汇编代码

Go 编译器会插入所谓的 duffzero 函数调用,以此来提高清零的效率达夫设备(Duff's device)

Slice

相比于array,slice可谓是开发使用频率最高的。不同于数组单单是一块连续内存;slice支持动态扩容,所以在底层的数据结构中就会有所变化。

  1. 指向底层数据的指针
  2. 切片长度
  3. 切片容量
type slice struct {
array unsafe.Pointer
len int
cap int
}

按照惯例看看汇编中slice是如何的

// data/slice/main.go
package main

func main() {
slice := []int{1, 2, 3, 4, 5}
_ = slice
}

大致总结一下步骤

  1. 栈空间初始化

  2. 向切片赋值

    1. 取地址

    2. 设置值

    3. 写入对应偏移量

  3. 构造切片结构(指针、长度、容量)

  4. 完成切片初始化

	# 栈空间初始化
0x000c 00012 STP (ZR, ZR), main..autotmp_2-72(SP)
0x0010 00016 STP (ZR, ZR), main..autotmp_2-56(SP)
0x0014 00020 MOVD ZR, main..autotmp_2-40(SP)

0x0018 00024 MOVD $main..autotmp_2-72(SP), R0
0x001c 00028 MOVD R0, main..autotmp_1-32(SP)
0x0020 00032 PCDATA $0, $-2
0x0020 00032 MOVB (R0), R27
0x0024 00036 PCDATA $0, $-1

# 常量1
0x0024 00036 MOVD $1, R1
0x0028 00040 MOVD R1, (R0)

0x002c 00044 PCDATA $0, $-2
0x002c 00044 MOVB (R0), R27
0x0030 00048 PCDATA $0, $-1

# 常量2
0x0030 00048 MOVD $2, R1
0x0034 00052 MOVD R1, 8(R0)

0x0038 00056 PCDATA $0, $-2
0x0038 00056 MOVB (R0), R27
0x003c 00060 PCDATA $0, $-1

# 常量3
0x003c 00060 MOVD $3, R1
0x0040 00064 MOVD R1, 16(R0)

0x0044 00068 PCDATA $0, $-2
0x0044 00068 MOVB (R0), R27
0x0048 00072 PCDATA $0, $-1

# 常量4
0x0048 00072 MOVD $4, R1
0x004c 00076 MOVD R1, 24(R0)

0x0050 00080 MOVD main..autotmp_1-32(SP), R0
0x0054 00084 PCDATA $0, $-2
0x0054 00084 MOVB (R0), R27
0x0058 00088 PCDATA $0, $-1

# 常量5
0x0058 00088 MOVD $5, R1
0x005c 00092 MOVD R1, 32(R0)
0x0060 00096 MOVD main..autotmp_1-32(SP), R0
0x0064 00100 PCDATA $0, $-2
0x0064 00100 MOVB (R0), R27
0x0068 00104 PCDATA $0, $-1
0x0068 00104 JMP 108

# 构造切片结构(指针、长度、容量)
0x006c 00108 MOVD R0, main.slice-24(SP) # 指向底层数组的地址(R0)
0x0070 00112 MOVD R1, main.slice-16(SP) # 长度
0x0074 00116 MOVD R1, main.slice-8(SP) # 容量

解开魔法 🪄

最后再来谈谈,掌握基础的汇编能给我们带来什么;不知道你在阅读的时候,会不会疑惑一些概念的真伪性或者说想去求证它。

这个时候当然是可以通过代码done的方式去验证;或者我们还可以使用汇编的方式去进行求证,以下案例是我在阅读《Go语言高级编程》所疑惑的

案例一

image

  • 代码方式进行验证
package main

import "fmt"

func main() {
array := [...]int{1, 2, 3}
array2 := array
array2[0] = 100
fmt.Println(array)
fmt.Println(array2)
}
  • 通过汇编的方式,我们只需要关注array2:=array的汇编代码
    • 很简单其实就是,这些指令将寄存器中的数据(如 R0, R1, R2)分别存储到栈上的不同位置,通常在处理数组或切片时,可能是将数组或切片的多个元素(或其它结构)存入栈空间。
	0x002c 00044 	MOVD	R0, main.array2-48(SP)
0x0030 00048 MOVD R1, main.array2-40(SP)
0x0034 00052 MOVD R2, main.array2-32(SP)

那么再来对比一下array2:=&array

  1. MOVD $main.array-32(SP), R0array的「地址」存储到寄存器R0中。
  2. R0的值保存到main.array2到栈位置(也就是说array2 只是存储了 array 的地址,它指向的是同一块内存空间)
	0x0024 00036 	MOVD	$main.array-32(SP), R0
0x0028 00040 MOVD R0, main.array2-8(SP)
0x002c 00044 PCDATA $0, $-2

案例二

image

package main

func main() {
array := [0]int{}
_ = array
}

不难发现,对于零长度的数组,Go 编译器通常会优化掉数组的实际分配,因此在汇编代码中看不到该数组的相关内容。

main.main STEXT size=16 args=0x0 locals=0x0 funcid=0x0 align=0x0 leaf
0x0000 00000 TEXT main.main(SB), LEAF|NOFRAME|ABIInternal, $0-0
0x0000 00000 FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x0000 00000 FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x0000 00000 RET (R30)
0x0000 c0 03 5f d6 00 00 00 00 00 00 00 00 00 00 00 00 .._.............

最后

通过汇编的方式,我们可以更加深入的了解Go语言的底层实现,也可以通过汇编的方式去验证一些疑惑的问题。当然,这里只是简单的介绍算是进行抛砖引玉。

后续可以关注此repo,会不定期更新一些有趣的案例,也欢迎大家一起探讨~

参考

https://xargin.com/go1-17-new-calling-convention/

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

https://taoshu.in/go/duff-zero.html

对分布式事务的一些思考

· 阅读需 16 分钟
OneCastle5280
Back-end Engineer @ SHOPLINE

要求

阅读这篇文章之前,需要对以下知识点有所了解~

  1. BASE 理论
  2. 事务的概念
  3. 分布式架构

1. 什么是分布式事务

是指在分布式系统中,事务的参与者、支持该事务的服务器、资源管理器和事务管理器分别位于不同的分布式节点之上。一个分布式事务通常涉及多个操作,这些操作可能跨越多个不同的数据库系统或服务,并且需要保证所有操作要么全部成功执行,要么全部不执行(即保持事务的原子性)。

例如:xx 电商系统的交易系统,承担用户下单功能;当用户对商品 A 进行购买的时候,假设商品 A 不允许超卖的场景,需要执行下面几个步骤:

  1. 检查库存:当用户提交订单时,系统首先调用库存服务检查所需商品是否有足够的库存。
  2. 锁定库存:如果库存充足,库存服务将临时锁定这些商品的数量,防止其他订单同时购买导致超卖。
  3. 检查积分:当用户选择使用积分抵扣现金时,系统需要调用积分服务检查用户是否有足够的积分进行抵扣。
  4. 锁定积分:如果积分充足,积分服务将临时锁定用户选择抵扣的积分数量,防止其他订单同时使用这份积分。
  5. 创建订单:订单服务创建一个新的订单记录,并设置订单状态为“待支付”。
  6. 发起支付:系统调用第三方支付网关交互以完成实际的支付过程。
  7. 确认支付并减少库存:支付成功后,系统通知库存服务正式减少商品库存,通知积分服务正式减少可使用积分,并更新订单状态为“已支付”。

涉及的服务有:

  1. 库存服务:负责管理商品的库存。
  2. 订单服务:负责处理用户的订单创建和对接第三方支付流程。
  3. 积分服务:负责处理用户的积分抵扣逻辑。

如果这些服务都是部署在同一台机器上,那就可以使用本地事务轻松控制一致性;但实际上述服务可能跑在不同的服务上,如下图所示: alt text

不同的服务部署在不同的节点上,节点之间通过 RPC 进行通讯;回到用户购买商品 A 这个流程,每个步骤都是由不同的服务执行的,用户想要购买成功,上述 5 个步骤都需要执行成功,如果任何一个步骤失败(例如库存不足、积分不足),那失败之前的步骤都需要进行回滚,例如锁定积分失败,那就需要将原本锁定成功的库存给释放掉,避免资源被长时间占用;

即用户购买商品的整个行为要么成功、要么失败。

2. TCC 是什么

Try-Confirm-Cancel,分布式事务的其中一种实现方式,是基于补偿机制实现的最终一致性;体现了 BASE 理论的一种实现方式;

2.1 整体架构

alt text

事务协调方

分布式事务的核心角色:负责管理和协调参与事务的各个服务或资源管理器;是分布式事务的发起方,需要确保所有参与者都能一致地提交或回滚事务。

资源方

分布式事务中负责管理本地资源(例如对于库存服务来说,库存就是本地资源)、执行资源锁定、核销或回滚操作。资源方都需要实现 Try()Confirm()Cancel() 三个接口, 下面简述一下每个接口需要承担的责任;

  1. Try(): 当分布式事务开启时,事务协调方会调用所有资源方的Try()进行「资源锁定」;资源方需要保证只要Try()调用成功,后续的ConfirmCancel()能够对「被锁定的资源」进行核销或释放。
  2. Confirm(): 用于提交分布式事务,核销前面通过Try()锁定的资源。
  3. Cancel(): 用于回滚分布式事务,释放前面通过Try()锁定的资源。

2.2 工作流程

下面描述一下正常的工作流程,即所有服务不出现异常的情况

2.2.1 分布式事务开启

分布式事务开启,事务协调方调用所有资源方的Try()接口,以上述用户购买商品作为示例: alt text

所有资源方都响应成功:

  1. 库存服务(资源方)锁定库存成功,库存状态标记为「锁定中」;
  2. 积分服务(资源方)锁定积分成功,积分状态标记为「锁定中」;
  3. 订单服务(事务协调方)发现所有资源方都响应成功,接着便开始创建订单,然后向支付渠道发起支付请求,订单标记为「未支付」;等待用户完成真正的支付。
    • 从发起支付到用户真正完成支付这个过程一般是异步的,订单服务需要等待支付的结果(成功/失败)来决定后续是对资源方发起 Confirm() 进行资源核销还是发起 Cancel() 进行资源释放。

2.2.2 分布式事务提交

alt text

假设用户顺利完成支付,订单服务则标识订单为「已支付」,并且对资源方发起Confirm() 请求对资源进行核销;

  1. 库存服务(资源方)正式扣减库存,库存状态标记为「已扣减」;
  2. 积分服务(资源方)正式扣减积分,积分状态标记为「已扣减」;

2.2.3 分布式事务回滚

alt text

假设用户放弃支付:订单服务则标识订单为「放弃支付」,并且对资源方发起Cancel() 请求对资源进行释放:

  1. 库存服务(资源方)释放锁定库存,库存状态标记为「已释放」;
  2. 积分服务(资源方)释放锁定积分,积分状态标记为「已释放」;

被释放出来的资源则可以继续被其他用户进行锁定-核销/释放

2.3 异常场景

上述都是各个服务都能够正常响应、正常处理业务逻辑的成功场景;一切都很美好,但实际上可能会存在各种各样的异常场景,例如:

  1. 有可能 Try() 请求应答成功了,但是后续的 Confirm()/Cancel() 无论怎么调也调用失败,资源一直处于锁定中。
  2. 有可能作为事务协调方的订单服务挂了,原本对库存服务已经调用Try()进行资源锁定,重启之后因为丢失了上一次调用的结果,又重新调用了一次,一个订单锁定了两份资源
  3. ....

我们针对每个步骤可能出现的异常做一下分析:

2.3.1 Try 阶段

部分资源方返回成功,部分资源方返回失败

情况1: alt text 资源方明确返回资源不充足,此时事务无法开启,需要通知Try()返回成功的资源方进行资源释放,即调用 Cancel().

情况2: alt text 超时导致的失败,则此次Try()调用有可能成功到达了资源方,也有可能因为网络问题最终没有到达资源方;但是对于事务协调方来说,就是调用 Try() 失败了;需要对所有资源方调用 Cancel() 进行资源释放。

情况3: alt text Try() 最终因为网络原因没有到达积分服务,此时接收到 Cancel() 请求,对于积分服务来说,没有任何资源可以支持回滚。

资源方的Cancel() 接口需要能够支持空回滚。

情况4: alt text

  1. 刚开始调用Try()超时了,事务协调方调用Cancel()进行资源释放。
  2. 在收到 Cancel() 之后,前面在网络中迷失的Try()请求又到达了积分服务;如果积分服务执行 Try() 成功,就会把资源给锁定了,并且对于事务协调方来说,分布式事务已经执行完成了,不会再有后续的 Confirm()/Cancel() 来对资源核销或者释放了;这个是因为网络原因导致的 乱序问题

资源方需要对每一次的Cancel()做好记录,当先执行的Cancel() 后执行的 Try() 的时候,能够识别不至于造成资源被锁定无法释放。

2.3.2 Confirm 阶段

作为事务协调方(订单服务),在进入Confirm 阶段之后,一定要确保通知所有资源方执行 Confirm() 进行事务的提交;

Confirm 调用失败

Try 阶段,因为存在网络/资源方服务状态这些不可控因素,无法确保 Confirm() 调用一定是成功的。 alt text 对于事务协调方来说,调用结果是失败的,积分服务是否成功无法感知,但是事务协调方不可能一直等待某个资源方的 Confirm() 响应成功。

此时,可以通过引入消息队列组件, 利用消息队列能够确保消息最低能够被消费一次的特性,让资源方自行监听消息,收到Confirm的消息,自行调用 Confirm() 逻辑, 完成Confirm阶段。如下图所示: alt text 这里同样有一个问题:订单服务(事务协调方)和消息队列 Broker同样是部署在不同的节点上,同样存在不确定性,如何确保消息一定发送出来呢?例如下列场景:

  1. Try 阶段完成,库存锁定成功、用户积分锁定成功、对应的订单创建成功,并且订单状态为「未支付」。
  2. 用户完成支付,此时事务要进入Confirm 阶段,开始调用资源方的 Confirm() 接口进行事务提交。
  3. 大部分情况下,调用都很顺利;此时事务顺利完成。
  4. 如果资源方的Confirm()接口调用失败,则往消息队列投递消息将其转化成异步消息实现最终一致,成功投递之后则更新订单状态为「已支付」,后续等待资源方消费消息完成资源核销即可。
  5. 调用 Confirm() 接口失败之后,转化成投递异步消息,假设这个时候消息队列挂了,消息发不出去了,没办法通知资源方了,并且如果订单服务需要一直等待这个发送成功,则订单状态一直无法更新为「已支付」。

针对上述第五步的情况,可以通过引入本地消息表 来解决,具体怎么做呢?

alt text

  1. 在调用资源方 Confirm() 接口出现异常的时候,借助消息队列最少能消费一次的特性,发送「补偿」消息,延后通知资源方进行核销, 实现最终一致性;并且此时订单状态更新为「已支付」。
  2. 如果发送「补偿」消息失败,则生成一条状态为「待发送」的消息记录到数据库,标识事务还有一个「补偿」消息没有完成发送;并且将存储「补偿」消息的动作和更新订单状态为「已支付」放到同一个本地事务里,要么一起成功、要么就一起失败;
  3. 然后另起一个定时任务来扫描「本地消息表」里「待发送」的消息记录,然后去做补偿发送消息。

2.3.3 Cancel 阶段

其实Cancel()阶段和Confirm()阶段可能产生的异常类似,事务协调方都需要保证Cancel()通知到位;

3. 最后

对于分布式架构的服务,整体成功率受到网络稳定性、各个节点服务自身的稳定性所影响,可能存在相同的请求重复请求多次、相同消息进行多次投递,这个要求各个资源方的 Try-Confirm-Cancel 接口需要做好幂等性;并且针对可能出现的请求乱序到达场景,需要做判断处理;其实上面只是列举到了一些常见的异常场景,还有可能存在更多更极端的情况,不能死记硬背,要理解每一种方式解决问题的本质是什么,可以用哪种方法来优化处理;

多多思考,多多学习

初探 Github Action

· 阅读需 10 分钟
wjc133
Back-end Engineer @ SHOPLINE

利用 Github Action 可实现博客变更时发送企业微信消息的功能。本文通过 .github/workflows/xxx.yml 文件自定义 Github Action 的 CI/CD 流程,通过 git diff 检查博客变更,使用 curl 发送企业微信通知。并且可以将这一过程封装成可复用的 Github Action,发布到 GitHub Marketplace,供其他人复用。

Plan9 & Go Assembler

· 阅读需 15 分钟
Agility6
Back-end Engineer @ SHOPLINE

Rob Pike 对 Go 语言设计理念的阐述中,一直在强调 Go 语言的简洁、清晰和高效。本文会通过揭示 Go 语言的底层实现,来理解语法糖的运作原理,并提供分析其他语言实现的思路。

有关于DMA

· 阅读需 16 分钟
wjc133
Back-end Engineer @ SHOPLINE

DMA 是一种硬件机制,其无需 CPU 干预即可实现外部设备与内存之间的数据传输,从而提高系统性能。在本文中,我详细总结了各种 DMA 控制器,并给出了 DMA 在 PC 和软盘设备之间交换数据的典型例程。

GO 内存模型

· 阅读需 20 分钟
OneCastle5280
Back-end Engineer @ SHOPLINE

与需要主动申请和释放内存的 C/C++ 不同,GO 实现了自动内存分配、逃逸分析和自动垃圾回收(GC),极大地解放了开发者的双手。在这篇文章中,我将为大家介绍 GO 语言的内存管理模型,深入了解 GO 在进行内存申请时是如何快速分配内存的。您将了解到 GO 语言在内存管理方面的独特设计和实现原理,帮助您更好地理解和应用 GO 语言进行高效编程。

有关于软盘控制器

· 阅读需 23 分钟
wjc133
Back-end Engineer @ SHOPLINE

在阅读李忠老师的《x86汇编:从实模式到保护模式》时,我遇到了一个关于硬盘读取的示例。由于实验环境的限制,我将示例改为读取软盘,但原程序无法正常运行。为了深入理解软盘的读取机制,我展开了一系列探索,并在这篇文章中记录了相关的研究过程和解决方案。本文不仅介绍了软盘控制器的相关寄存器作用,还分别展示了使用 BIOS 中断和不使用 BIOS 中断的方法来读取软盘数据。希望通过这篇文章,能帮助大家更好地理解软盘控制器的工作原理和实际应用。

远程使用 Linux Server 上的 Bochs

· 阅读需 4 分钟
wjc133
Back-end Engineer @ SHOPLINE

我手里有一台 M1 芯片的 MBP,学习 x86 汇编需要一个 x86 的机器,恰好我有一台实验机,安装的是 ubuntu server 24.04 版本。由于 Server 本身没有图形界面,也没有连接任何显示器。就想试试能不能通过远程的方式运行 bochs。

什么是黏包,半包又是什么?

· 阅读需 11 分钟
OneCastle5280
Back-end Engineer @ SHOPLINE

最近在学习如何手写一个 tiny-mq,其中在写网络通讯模块的时候,了解到黏包和半包的概念,并且需要对其进行处理;这篇文档就简单聊一聊什么是黏包、半包,产生的原因是什么,对我们有什么影响,我们应该如何来处理