深入理解 go unsafe

news2024/11/17 10:33:44

往期精选文章推荐:

  1. 深入理解 go map
  2. go 常用关键字
  3. 深入理解 Go 数组、切片、字符串
  4. 深入理解channel
  5. 深入理解 go context
  6. 深入 go interface 底层原理
  7. 深入理解 go reflect
  8. 深入理解 go unsafe

为什么有go unsafe

Go 是支持指针的语言,但是为了保持简洁、安全,Go 的指针有很多限制,但是一些场景又需要绕过这些安全限制,因此提供了 unsafe 包,unsafe 可以绕过:指针运算、不同的指针类型不能转换、任意类型指针等限制,进而可以实现更高级的功能。

下面通过对比可以更直观的了解 unsafe 的特点:

支持指针运算

Go 的指针是不支持指针的,指针运算在直接操作内存有很大作用,例如:通过指针运算访问数组元素、或者通过指针运算访问结构体的导出字段等等。

go 指针

var arr  = [3]int{1, 2, 3}
ptr := &arr
fmt.Println(ptr++) // 报错

$ go run main.go
//./main.go:8:17: syntax error: unexpected ++, expecting comma or )

unsafe.Pointer

var arr  = [3]int{1, 2, 3}
ptr := (unsafe.Pointer(uintptr(unsafe.Pointer(&arr)) + unsafe.Sizeof(arr[0])))
fmt.Println(*(*int)ptr)

$ go run main.go
2

支持不同的指针类型转换

go 指针

// go 指针
type MyInt int

func main() {
    var num int = 5
        var miPtr *MyInt
        miPtr = &num
}

$ go run main.go
./main.go:12:8: cannot use &num (type *int) as type *MyInt in assignment

unsafe.Pointer

// unsafe.Pointer
type MyInt int

func main() {
    var num int = 5
        var miPtr *MyInt
        miPtr = (*MyInt)(unsafe.Pointer(&num))
        fmt.Println(*miPtr)
}
$ go run main.go
5

支持任意类型指针

unsafe.Pointer 的定义如下,其语义是任意类型的指针:

type Pointer *ArbitraryType

unsafe.Pointer 在 Go 源码中有广泛的应用,例如接口的底层数据结构:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

data 字段就是 unsafe.Pointer 类型,意味着 data 可以存储任意类型的数据。

什么是 go unsafe

unsafe 是 Go 语言中的一个包,用于进行一些底层的、不安全的操作。

在 Go 语言中,通常强调的是安全性和内存安全性,以防止常见的编程错误,比如缓冲区溢出和悬空指针等。但在某些特定的场景下,可能需要突破这些安全限制来实现一些特殊的功能或优化性能。

使用 unsafe 包时需要非常谨慎,因为不正确的使用可能会导致程序出现难以调试的错误,甚至破坏程序的稳定性和安全性,应仅在有充分理由和完全理解其风险的情况下使用。

Unsafe 包比较简单,主要是通过 Pointer、Sizeof、Offsetof、Alignof 来实现一些高级特性。

unsafe.Pointer

可以说 unsafe.Pointer 是 unsafe 包里的重中之重,没有 unsafe.Pointer unsafe 包就没有存在的意义。我们先看一下unsafe.Pointer 的定义:

// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int

type Pointer *ArbitraryType

unsafe.Pointer 表示指向任意类型的指针。

什么是 uintptr

其实 unsafe.Pointer 本身并不支持指针运算,需要借助 uintptr 来实现指针运算,uintptr 的定义如下:

// uintptr 是一种整数类型,具有足够大小,可以保存任何指针类型。
type uintptr uintptr

uintptr 在源码中有广泛的应用,一般用来保存整数形式的内存地址,因为 uintptr 是整数类型所以它可以进行运算:

func main() {
    var num1 int = 5
    var num2 int = 5
    p1 := uintptr(unsafe.Pointer(&num1))
    p2 := uintptr(unsafe.Pointer(&num2))

    fmt.Println("num1 的内存地址:", p1)
    fmt.Println("num2 的内存地址:", p2)
}

$ go run main.go
num1 的内存地址: 824634183432
num2 的内存地址: 824634183424

但是有一点需要注意:uintptr 并不具有指针语义,也就是 uintptr 保存的内存地址中的内容是可能被 GC 回收的。

unsafe.Pointer 类型转换

unsafe.Pointer 类型有四种特殊操作:

  1. 任何类型的指针值都可以转换为unsafe.Pointer。

  2. unsafe.Pointer 也可以转换为任何类型的指针值。

  3. uintptr 可以转换为 unsafe.Pointer。

  4. unsafe.Pointer 可以转换为 uintptr。

因此,unsafe.Pointer 允许程序突破类型系统并读写任意内存,应谨慎使用unsafe.Pointer。

unsafe.Pointer 使用模式

unsafe.Pointer 如同双刃剑,在赋予强大功能的同时也伴随着风险。官方包中提供了几种相对安全的使用模式,然而即使遵循这些模式,也无法确保绝对的安全性。

使用 “go vet” 可以帮助找到不符合这些模式的 Pointer 的使用,但是“go vet”的沉默并不能保证代码有效。

(1)将 *T1 转换为指向 *T2 的指针。

如果想将 *T1 转换为 *T2 需要满足两个条件:

  1. 假设 unsafe.Sizeof(T2) 小于等于 unsafe.Sizeof(T1);

  2. T1 和 T2 具有相同的内存布局(不是完全相同,保证T2大小范围内布局相同就行)。

type T1 struct {
    Name     string
    Age      int
    Language string
}

type T2 struct {
    Name string
    Age  int
}

func main() {
    t1 := &T1{Name: "xiaoming", Age: 18, Language: "golang"}
    t2 := (*T2)(unsafe.Pointer(t1))

    fmt.Println(t2.Name, t2.Age)
}

$ go run main.go
xiaoming 18

(2)将指针转换为 uintptr(但不能转换回指针)。

将 Pointer 转换为 uintptr 会得到 Pointer 指向的内存地址,是一个整数。但是,uintptr 转换回 Pointer 通常无效。uintptr 是一个整数,而不是一个引用。将指针转换为 uintptr 会创建一个没有指针语义的整数值。 即使 uintptr 持有某个对象的地址,如果该对象移动,垃圾收集器也不会更新该 uintotr 的值, 也不会阻止该对象被回收。

如下面这种,我们取得了变量的地址 p,然后做了一些其他操作,最后再从这个地址里面读取数据:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var a int = 10
    var p = uintptr(unsafe.Pointer(&a))
    // ... 其他代码
    // 下面这种转换是危险的,因为有可能 p 指向的对象已经被垃圾回收器回收
    fmt.Println(*(*int)(unsafe.Pointer(p)))
}

(3)通过算术运算,将 unsafe.Pointer 转换为 uintptr 并转换回来。

如果 p 指向一个已分配的对象,我们可以将 p 转换为 uintptr 然后加上一个偏移量,再转换回 Pointer。如:

p = unsafe.Pointer(uintptr(p) + offset)

此模式最常见的用途是访问结构中的字段或数组中的元素:

// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))

// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

这种模式有几个注意点:

  1. 将指针加上一个超出其原始分配的内存区域的偏移量是无效的:
// INVALID: end points outside allocated space.
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))

// INVALID: end points outside allocated space.
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))
  1. Pointer => uintptr, uintptr => Pointer两种转换必须出现在同一个表达式中,并且它们之间只有中间的算术:
// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := uintptr(p)
p = unsafe.Pointer(u + offset)
  1. unsafe.Pointer必须指向分配的对象,因此它不能为零。
// INVALID: conversion of nil pointer
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)

(4)调用 syscall.Syscall 时将指针转换为 uintptr。

syscall 包中的 Syscall 函数将其 uintptr 参数直接传递给操作系统,然后操作系统可能会根据调用的细节将其中一些参数重新解释为指针。也就是说,系统调用实现会隐式地将某些参数从 uintptr 转换回指针。

如果一个指针参数必须转换为 uintptr 以用作参数,那么该转换必须出现在调用表达式本身中:

syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

(5)将 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 的结果从 uintptr 转换为 Pointer。

reflect.Value 的 Pointer 和 UnsafeAddr 方法返回类型 uintptr 而不是 unsafe.Pointer, 从而防止调用者在未引入 unsafe 包的情况下将结果更改为任意类型。(这是为了防止开发者对 Pointer 的误操作。) 然而,**这也意味着这个返回的结果是脆弱的,我们必须在调用之后立即转换为 **Pointer(如果我们确切的需要一个 Pointer):

p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

与上述情况一样,存储转换之前的结果是无效的:

// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := reflect.ValueOf(new(int)).Pointer() // u 指向的内存可能被回收
p := (*int)(unsafe.Pointer(u))

(6)reflect.SliceHeader或reflect.StringHeader数据字段与unsafe.Pointer互转

与前一种情况一样,反射数据结构 SliceHeader 和 StringHeader 将字段 Data 声明为 uintptr,以防止调用者在未先引入“unsafe”的情况下将结果更改为任意类型。但是,这意味着 SliceHeader 和 StringHeader 仅在解释实际切片或字符串值的内容时有效。

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n

unsafe.Sizeof

// src/unsafe/unsafe.go
func Sizeof(x ArbitraryType) uintptr

Sizeof 返回任意类型变量 x 所占用内存字节大小,这个大小不包含 x 底层引用的内存大小。例如,如果 x 是一个切片,那么 Sizeof 返回的是切片描述符的大小,而非切片所引用的内存的大小。

func main() {
    var num1 int8 = 5
    var num2 int16 = 5
    
    var slice1 []int = []int{1, 2, 3}
    var slice2 []string = []string{"hello", "wrold"}
    
    fmt.Println("num1 sizeof: ", unsafe.Sizeof(num1))
    fmt.Println("num2 sizeof: ", unsafe.Sizeof(num2))
    fmt.Println("slice1 sizeof: ", unsafe.Sizeof(slice1))
    fmt.Println("slice2 sizeof: ", unsafe.Sizeof(slice2))
}

$ go run main.go
num1 sizeof:  1
num2 sizeof:  2
slice1 sizeof:  24
slice2 sizeof:  24

unsafe.Offsetof

// src/unsafe/unsafe.go
func Offsetof(x ArbitraryType) uintptr

unsafe.Offsetof 返回 x 表示的字段在结构体中的偏移量,该字段必须采用 structValue.field 形式。换句话说,它返回结构体开头和字段开头之间的字节数。

type Programmer struct {
    Name     string
    Age      int
}

func main() {
    p := Programmer{}
    NameOffset, AgeOffset := unsafe.Offsetof(p.Name), unsafe.Offsetof(p.Age)
    fmt.Println("name offset: ", NameOffset)
    fmt.Println("age offset: ", AgeOffset)
}

$ go run main.go
name offset:  0
age offset:  16

unsafe.Alignof

// src/unsafe/unsafe.go
func Alignof(x ArbitraryType) uintptr

Alignof 返回某一个类型的对齐系数,就是对齐一个类型的时候需要多少个字节。 它与reflect.TypeOf(x).Align()返回的值相同。作为一种特殊情况,如果变量 s 是结构体类型并且 f 是该结构体中的字段,则 Alignof(s.f) 将返回结构体中该类型字段所需的对齐方式。这种情况与reflect.TypeOf(s.f).FieldAlign()返回的值相同。

type Programmer struct {
    Name     string
    Age      int
}

func main() {
    p := Programmer{}
    fmt.Println("age Alignof: ", unsafe.Offsetof(p.Age))
}

$ go run main.go
age Alignof:  16

后面会有专门文章讨论内存对齐,这里不详细展开。

unsafe 示例

[]byte与string 高效互转

我们知道slice 和 string 的底层数据结构非常类似,并且 []byte 和 string 都是基于字符数组实现的,所以通过共享底层字符数组技能实现零拷贝的转换。

slice 和 string 的底层数据结构:

type StringHeader struct {
        Data uintptr
        Len  int
}

type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}

[]byte与string 高效互转的实现:

func string2bytes(s string) []byte {
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{
            Data: stringHeader.Data,
            Len:  stringHeader.Len,
            Cap:  stringHeader.Len,
    }

    return *(*[]byte)(unsafe.Pointer(&bh))
}

func bytes2string(b []byte) string{
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    sh := reflect.StringHeader{
            Data: sliceHeader.Data,
            Len:  sliceHeader.Len,
    }

    return (string)(unsafe.Pointer(&sh))
}

但是需要注意的是使用 string2bytes 时如果入参字符串所引用字符数组是不可更改的情况,转换后[]byte也是不可修改的:

package main

import (
    "fmt"
    "unsafe"
    "reflect"
)

func main() {
        s := "hello" // 字符串字面量被存储在只读的数据段中。
        bs := string2bytes(s)
        bs[0] = 'a' // 这块会编译报错,不可修改
        
        fmt.Println(bs[0])
}

$ go run main.go

直接访问数组的元素

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    ptr := unsafe.Pointer(&arr[0])
    for i := 0; i < len(arr); i++ {
        value := *(*int)(unsafe.Pointer(uintptr(ptr) + uintptr(i)*unsafe.Sizeof(arr[0])))
        fmt.Println(value)
    }
}

通过数组的起始地址 + 每个元素的偏移量就能访问数组元素。

直接访问切片的元素

访问切片元素和访问数组元素的方式差不多,但是需要注意的是:要是使用 &slice[0]代表切片的起始位置,而数组直接使用数组指针就行。

arr := [5]int{1, 2, 3, 4, 5}
slice := []int{1, 2, 3, 4, 5}

如下图所示:&arr代表数组的起始位置,&slice 代表切片结构体的起始位置, &slice[0]代表切片底层数组的起始位置。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    slice := []int{1, 2, 3, 4, 5}
    // 获取切片底层数组的指针
    ptr := unsafe.Pointer(&slice[0])
    ptr = unsafe.Pointer(uintptr(ptr) + uintptr(2)*unsafe.Sizeof(slice[0]))
    // 直接修改底层数组的值
    *(*int)(ptr) = 10
    fmt.Println(slice)
}

$ go run main.go
[1 2 10 4 5]

访问结构体未导出字段

同样是通过起始地址加上偏移量就能访问结构体字段:

// 其他包里面定义的结构体
type Programmer struct {
    name     string
    age      int
    language string
}
// 在 main 包里引用结构体
func main() {
    p := programmer.Programmer{}
    fmt.Println(p)

    name := (*string)(unsafe.Pointer(&p))
    age := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(string(""))))
    lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string(""))))

    *name = "Tom"
    *age = 23
    *lang = "Golang"

    fmt.Println(p)
}

$ go run main.go
{ 0 }
{Tom 23 Golang}

由于我们访问的是未导出字段,因此无法使用unsafe.Offsetof,只能通过将字段大小相加的方式来计算偏移量。然而,这样做存在一个问题:在计算偏移量时,如果存在内存对齐,那么计算出的偏移量就不准确了。因此,我们还需要加上内存对齐的偏移量。不同平台上的内存对齐结果也可能不同。

绕过类型检查

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var num1 int = 10
    var num2 float64
    // 绕过类型检查进行赋值
    num2 = *(*float64)(unsafe.Pointer(&num1))
    fmt.Println(num2)
}

$ go run main.go
5e-323

这种转换的结果往往不是我们想要的,大家一定要小心使用。

参考

https://www.cnblogs.com/qcrao-2018/p/10964692.html

https://pkg.go.dev/unsafe

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

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

相关文章

HW高耗电提醒竞品调研

摘要 高耗电提醒通知的规则,天生存在打扰用户的特点,故在触发高耗电检测阈值还要根据是否非可感知场景,进一步修正高耗电提醒的准确率。同时消息通知的交互设计中也进行少打扰静默设计 一、功耗高耗电通知监控规则 1.1 高耗电上报规则和文案 支持的耗电类型 上报高耗电通…

CTF-mysql

整数型 输入1发现有回显 发现and11有回显12没有 判断字段数 1 order by 2 确定回显点 -1 union select 1,2 查看数据库名称 -1 union selecr 1,database() 查看数据库名 -1 union slelct group_concat(schema_name)from information_schema.schemata 查看表名 -1 union s…

MySQL进阶难度知识点分析

以下为本人在阅读《MySQL是怎样运行的&#xff1a;从根儿上理解MySQL》这本书时对一些难度和重点的笔记&#xff0c;主要用于个人学习使用&#xff0c;内容可能存在出入&#xff0c;望理性食用~ 1. sql执行流程 一条sql的执行流程大致可分为客户端获取与数据库服务器的连接&am…

使用 Hugging Face Transformers 创建文本生成模型

文本生成是自然语言处理中的一个重要任务&#xff0c;在聊天机器人、自动写作等领域有着广泛的应用。Hugging Face Transformers 是一个流行的 Python 库&#xff0c;它提供了大量预训练的模型以及API来实现各种自然语言处理任务。本文将详细介绍如何使用 Hugging Face Transfo…

Golang | Leetcode Golang题解之第338题比特位计数

题目&#xff1a; 题解&#xff1a; func countBits(n int) []int {bits : make([]int, n1)for i : 1; i < n; i {bits[i] bits[i&(i-1)] 1}return bits }

工业三防平板在数字化工厂建设中的重要趋势

在当今数字化浪潮的冲击下&#xff0c;工厂建设的数字化转型已,成为不可逆转的趋势。而在这一进程中&#xff0c;工业三防平板正逐渐斩露头角&#xff0c;发挥着越来越重要的作用。随着工业4.0理念的不断深入&#xff0c;工厂对于生产效率、质量控制、管理精细化的要求越来越高…

Elasticsearch核心概念:

2.Elasticsearch核心概念: 2.1.Lucene和Elasticsearch的关系: 1.Lucene&#xff1a;最先进、功能最强大的搜索库&#xff0c;直接基于lucene开发&#xff0c;非常复杂&#xff0c;api复杂2.Elasticsearch&#xff1a;基于lucene&#xff0c;封装了许多lucene底层功能&#xf…

2-67 基于matlab的经典数字图像处理算法仿真

基于matlab的经典数字图像处理算法仿真&#xff0c;17页文档报告。包括图像的傅里叶滤波及压缩&#xff0c;图像的DCT高通、低通滤波&#xff0c;图像直方图均衡化&#xff0c;图像平滑与锐化&#xff0c;图像的模糊化&#xff0c;哈夫曼编码等&#xff0c;以及GUI图形化界面。…

鸿蒙内核源码分析(任务管理篇) | 任务池是如何管理的?

任务即线程 在鸿蒙内核中&#xff0c;广义上可理解为一个任务就是一个线程 官方是怎么描述线程的 基本概念 从系统的角度看&#xff0c;线程是竞争系统资源的最小运行单元。线程可以使用或等待CPU、使用内存空间等系统资源&#xff0c;并独立于其它线程运行。 鸿蒙内核每个…

在Linux中进行supervisor进程守护的安装和配置

supervisor用于守护进程&#xff0c;在进程意外终止后将其重启。 supervisor没有监听内部程序和自动重启的功能。 Python-3.9.5安装 第一步&#xff0c;检查Linux系统是否自带Python。 命令&#xff1a;python --version 第二步&#xff0c;安装依赖包。 命令&#xff1a;…

Java超市收银系统(八、数据导入)

引言 当选择1时&#xff0c;程序读取 “商品信息.xls” 文件&#xff0c;将所有数据存放于product集合中&#xff0c;然后将集合中的所有数据增加到商品表中&#xff0c;增加的时候要检查每条记录的条形码在商品表中是否存在&#xff0c;若存在&#xff0c;则不需要增加到数据库…

tortoisegit下载及其使用流程

下载 官方下载链接&#xff1a;Download – TortoiseGit – Windows Shell Interface to Git 选择适合自己的电脑位数的版本&#xff1a;一般64的兼容32的 按照就不介绍了怎么开心怎么来&#xff0c;本篇暂时为了支持一位粉丝的疑惑 安装的话没有特殊配置暂不介绍&#xff0c…

Dbeaver连接达梦数据库教程(图文版)

本章教程&#xff0c;主要介绍如何用Dbeaver连接国产达梦数据库。 达梦数据库Docker部署教程参考&#xff1a;https://yang-roc.blog.csdn.net/article/details/141158807 一、Dbeaver安装包下载 下载Dbeaver&#xff1a;https://dbeaver.io/ 在这里就不演示安装过程了&#xf…

GPU驱动的大规模静态物件渲染

GPU Driven 的静态物件渲染&#xff0c;听起来很高级&#xff0c;其实具体操作很简单&#xff0c;基础就是直接调用 Graphics.DrawMeshInstancedIndirect 这个 Unity 内置接口就可以了。但我们项目对这个流程做了一些优化&#xff0c;使得支持的实体数量有大幅提升。 这套系统主…

海南云亿商务咨询有限公司引领抖音电商新潮流

在当今这个数字化时代&#xff0c;电商行业如日中天&#xff0c;而抖音作为短视频与社交电商完美融合的典范&#xff0c;正以前所未有的速度改变着人们的购物习惯和消费模式。在这片充满机遇与挑战的蓝海中&#xff0c;海南云亿商务咨询有限公司凭借其敏锐的市场洞察力和专业的…

【算法/学习】:flood算法

✨ 君子坐而论道&#xff0c;少年起而行之 &#x1f30f; &#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;算法学习 &#x1f680; 欢迎关注&#xff1a;&#x1f44d;点赞 &…

鸿蒙交互事件开发01——点击/拖拽/触摸事件

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1 概 述 事件是人机交互的基础&#xff0c;鸿蒙开发中&#xff0c;事件分…

EmguCV学习笔记 VB.Net 2.1 颜色空间和颜色

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 EmguCV学习笔记目录 Vb.net EmguCV学习笔记目录 C# 笔者的博客网址&#xff1a;VB.Net-CSDN博客 教程相关说明以及如何获得pdf教程…

威胁组织伪造Loom,Mac用户警惕AMOS窃取软件威胁

近期&#xff0c;一个复杂且可能与神秘威胁组织“Crazy Evil”有关联的网络犯罪活动&#xff0c;已将注意力转向了Mac用户群体。该组织利用广受欢迎的屏幕录制工具Loom作为掩护&#xff0c;悄无声息地传播着臭名远扬的AMOS数据窃取软件。Moonlock Lab的安全研究员们揭开了这一阴…

【数据结构与算法 | 图篇】拓扑排序

1. 概念 拓扑排序是是一种针对有向无环图进行的排序方法。它将图中所有顶点排成一个线性序列&#xff0c;使得对于图中的每一条有向边(u, v)&#xff0c;顶点u在序列中都出现在顶点v之前。 适用范围&#xff1a; 拓扑排序只适用于有向无环图。 结果非唯一&#xff1a; 对于…