当前位置: 首页 > news >正文

厦门的企业网站wordpress查看版本

厦门的企业网站,wordpress查看版本,北碚区网站建设,湘潭网站建设磐石网络Go 程序的启动流程 本文将以一个简单的 HelloWorld 程序为例#xff0c;探究 Go 程序的启动流程 package mainfunc main() {_ Hello World }入口 我们先通过 go build . 将代码编译成可执行文件#xff0c;众所周知#xff0c;我们在一个 shell 中执行可执行…Go 程序的启动流程 本文将以一个简单的 HelloWorld 程序为例探究 Go 程序的启动流程 package mainfunc main() {_ Hello World }入口 我们先通过 go build . 将代码编译成可执行文件众所周知我们在一个 shell 中执行可执行文件时shell 进程会启动一个子进程并在子进程中通过 execve 系统调用启动加载器加载器会读取可执行文件头部信息中的入口点(entry point) 字段该字段告诉加载器应该从什么地方开始执行该可执行文件这便是我们要找的程序入口。 不同环境的可执行文件格式不同查看入口点的方法也不同以最常见的 Linux 系统的 TLF 格式为例 先将代码交叉编译成 TLS 格式 GOOSlinux GOARCHamd64 go build -gcflags-N -l -o main使用 objdump -f 便可以得到入口点地址为 0x0000000000453860 ➭ objdump -f main main: file format elf64-x86-64 architecture: x86_64 start address: 0x0000000000453860使用 -t 列出符号表并检索入口地址就可以得到 go 程序的入口地址 _rt0_amd64_linux ➭ objdump -t main | grep 453860 0000000000453860 g F .text 0000000000000005 _rt0_amd64_linux如果你是 Mac 系统可执行文件格式为 Mach-O 想要查看这种文件的入口点信息可以使用 otool 工具查看Mach-O 中这个信息被放在 Load Command 中使用命令 ➭ otool -l main | awk /entryoff/ { printf 0x%x\n, $2 } 0x4fe80得到入口点偏移地址为 0x4fe80 同样检索符号表可以得到入口函数 _rt0_arm64_darwin ➭ otool -t -V helloworld | grep -C 1 4fe80 __rt0_arm64_darwin: 000000010004fe80 adrp x2, 0 ; 0x10004f000 000000010004fe84 add x2, x2, #0x160不管是 AMD 的 _rt0_amd64_linux 还是 ARM 的 _rt0_arm64_darwin 在简单的移动 argc 和 argv 之后都会跳转到 rt0_go 函数这就是引导 Go 程序的入口点它主要干以下几件事 初始化 TLS初始化 g0 和 m0 并相互绑定初始化命令行参数,程序执行路径操作系统初始化调度器初始化启动新的 groutine 执行 main 函数 后续内容以 ARM 架构的 Mac 系统为例 初始化 TLS TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0SUB $32, RSPMOVW R0, 8(RSP) // argcMOVD R1, 16(RSP) // argv#ifdef TLS_darwin// Initialize TLS.MOVD ZR, g // clear g, make sure its not junk.SUB $32, RSPMRS_TPIDR_R0AND $~7, R0MOVD R0, 16(RSP) // arg2: TLS baseMOVD $runtime·tls_g(SB), R2MOVD R2, 8(RSP) // arg1: tlsgBL ·tlsinit(SB)ADD $32, RSP #endifrt0_go 中会先将寄存器中的 argc 和 argv 移动到栈上之后进入初始化 TLS 的过程。 TLSThread Local Storage线程本地存储被用来保存那些只对本线程可见的全局变量go 会将正在系统线程上运行的 groutine 保存在 TLS 中在实现上一般会在内存中开辟一块专门的区域用来保存 TLS 数据然后使用某个 CPU 寄存器保存这块内存的起始地址AMD 架构下一般使用段寄存器 FD 而 ARM 有一个专门的 TPIDR 寄存器来保存线程被操作系统调度时会保存和恢复寄存器的值这样在任何情况下根据寄存器中的起始地址我们一定可以拿到专属于该线程的全局数据。 在 c 中可以使用 __thread 将变量声明为 “Pre-Thread” 的 POSIX 提供了一组 API1 来操作 TLS // 存储 每线程 的变量 int pthread_setspecific(pthread_key_t key, const void *value);// 获取 每线程 的变量 void *pthread_getspecific(pthread_key_t key);// 生成一个 每线程 变量的 key int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));它的内部结构类似一个 uintptr 的数组每一位都保存一个指向真实数据地址的指针而根据 pthread_key 则可以为宜定位到该数组中的某一位。 接下来我们逐行分析这段汇编 MOVD ZR g这一行的作用是将 g 清空避免脏数据。 ZR 是一个 62 bit 零寄存器的代称它总是返回 0。 g 是 go 官方对 ARM 架构下 R10 寄存器的一个代称可能因为他们总是使用 R10 来保存 g 的指针因此在 go 的汇编中直接使用 R10 是无效的只能使用 g 指代.1 这一句翻译一下就是清空 R10 寄存器。 SUB $32, RSP栈指针 RSP 向下移 32 字节为后面的函数调用申请栈空间。 MRS_TPIDR_R0 AND $~7, R0这实际上是一个宏定义在 tls_arm64.h 中, 作用是读 TPIDR 寄存器的值到 R0 TPIDR 寄存器中保存的就是 TLS 的基地址 #define MRS_TPIDR_R0 WORD $0xd53bd040 // MRS TPIDR_EL0, R0下面的 AND 是 ARM 的特殊规定要求基地址低 3 位必须为 0 MOVD R0, 16(RSP) // arg2: TLS base MOVD $runtime·tls_g(SB), R2 MOVD R2, 8(RSP) // arg1: tls BL ·tlsinit(SB)这四行的作用是调用 tlsinit 函数为 tls_g 赋值。 func tlsinit(tlsg *uintptr, tlsbase *[_PTHREAD_KEYS_MAX]uintptr) {}这个函数接受两个参数 tlsg: tlsg 的指针来自 runtime·tls_g(SB) 这是一个全局的变量tlsbase: 从寄存器中取出的 TLS 基地址 它会调用 POSIX API 在 TLS 中临时保存一个魔法值之后遍历 TLS 空间找到这个保存这个魔法值的槽的偏移并将其赋值给 runtime·tls_g 上面说过POSIX 的 TLS 实现类似于一个数组这里 runtime·tls_g 中保存的就是 g 在那个数组中的地址偏移这样寄存器中的 TLS 基地址加上这里的地址偏移就可以定位到 g 的地址了。 ADD $32, RSP最后函数调用结束释放栈。 总结一下 相对而言 ARM 初始化 TLS 的过程是比较简单的总体就做了一件事为 runtime·tls_g 赋值 这个全局变量保存了 g 在 tls 中的偏移根据 tls 基地址加上偏移我们就可以定位到保存在 TLS 中的 g 了 初始化 g0 m0 我们知道 g 是 go 对 groutine 的抽象m 是对系统线程的抽象。 而 m0 描述的就是主线程运行中主线程上的第一个 groutine 就是 g0. m0 和 g0 是两个全局变量引导时会使用汇编为这两个变量中的一些字段赋初值 // runtime/proc.go var (m0 mg0 g )MOVD $runtime·g0(SB), g MOVD RSP, R7 MOVD $(-64*1024)(R7), R0 MOVD R0, g_stackguard0(g) MOVD R0, g_stackguard1(g) MOVD R0, (g_stackstack_lo)(g) MOVD R7, (g_stackstack_hi)(g)这里是在初始化 g0 的栈帧 type stack struct {lo uintptr // gorutine 栈低地址hi uintptr // groutine 栈高地址 }type g struct {stack stack // 标识栈的边界stackguard0 uintptr // 同于栈增长stackguard1 uintptr // TODO }执行完上述代码后程序的栈空间应该如下 high addr┌───────────┐│ ││ ││ │├───────────┤│ argv 8 │├───────────┤ RSP │ argc 8 │ ─────►├───────────┤◄─────▲ │ │ g0.stack.ho│ │ ││ │ ││ │ ││ │ ││ │ ││ │ │ 64kb │ g0 stack ││ │ ││ │ ││ │ ││ │ ││ │ ││ │ ││ │ │ g0.stackguard1▼ │ │ g0.stackguard0 ─────►├───────────┤◄───── R7 │ │ g0.stack.lo│ ││ ││ ││ │└───────────┘low addr初始化栈后会调用 save_g 正式将 g0 的地址存入 TLS nocgo:BL runtime·save_g(SB)TEXT runtime·save_g(SB),NOSPLIT,$0MRS_TPIDR_R0MOVD runtime·tls_g(SB), R27MOVD g, (R0)(R27)和上面一样 R0 是从寄存器中获得的 TLS 基地址R27 是来自 tls_g 的 TLS 偏移通过 R0R27 的间接寻址得到的就是 g0 在 TLS 中的位置 接着会调整 stackguard0 和 stackguard1 以插入栈保护区StackGuard 默认是 928 字节 MOVD (g_stackstack_lo)(g), R0 ADD $const__StackGuard, R0 MOVD R0, g_stackguard0(g) MOVD R0, g_stackguard1(g)接下来会绑定 g0 和 m0: MOVD $runtime·m0(SB), R0MOVD g, m_g0(R0) // m0.g0 g0MOVD R0, g_m(g) // g0.m m0type m struct {g0 *g }m 的 g0 字段保存了当前正在我上面执行的是哪个 groutine 而 g 结构中也有一个 m 字段用来反向查找当前 groutine 在哪个 m 上执行 总结 这一段主要做了三件事 为 g0 申请栈空间32 KB其中有 928 字节保护区将 g0 地址保存到 TLS 中将 m0 和 g0 双向绑定 这时的内存布局为 high addr┌───────────┐│ ││ ││ │├───────────┤│ argv 8 │├───────────┤ RSP │ argc 8 │ ─────►├───────────┤◄─────▲ │ │ g0.stack.ho│ │ ││ │ ││ │ ││ │ ││ │ ││ │ │ 64kb │ g0 stack ││ │ ││ │ ││ │ ││ │ ││ │ │ g0.stackguard0│ │───────────│◄───── g0.stackguard1│ │ 928 byte │ ▼ │ │ 栈保护区 ─────►├───────────┤◄───── R7 │ │ g0.stack.lo│ ││ ││ ││ │└───────────┘low addrargs/osinit TLS 初始化结束后会调用 args 对命令行参数进行初始化 MOVW 8(RSP), R0 // copy argc MOVW R0, -8(RSP) MOVD 16(RSP), R0 // copy argv MOVD R0, 0(RSP) BL runtime·args(SB)BL runtime·osinit(SB) BL runtime·schedinit(SB)var (argc int32argv **byte )//go:linkname executablePath os.executablePath var executablePath stringfunc args(c int32, v **byte) {argc cargv vsysargs(c, v) }值得注意的是这一步只是调用 sysargs 初始化了 executablePath 全局变量它保存的是当前可执行文件的绝对路径在代码中你可以使用 os.Executable() 获取到该值。 argv 参数在程序中可以使用 os.Argv 获取返回的是一个 []string 不过这一步并没有初始化这个值它与 os.GetEnv() 所返回的环境变量的值一起会在调度器初始化时通过 goargs 和 goenvs 函数初始化。 实际上命令行参数 argv 和环境变量 envv 都是按顺序保存在栈上的v **byte这一步将它赋值给了全局变量 argv, 它的结构如下 ┌───────────────┬───┬────────────────────┬───┬───────────────────────────┬───┐ │ │ n │ │ n │ │ n │ │ │ u │ │ u │ │ u │ │ argv │ l │ envv │ l │ executablePath │ l │ │ │ l │ │ l │ │ l │ │ │ │ │ │ │ │ └───────────────┴───┴────────────────────┴───┴───────────────────────────┴───┘程序参数被初始化后就会通过 osinit 初始化 cpu 数量和物理内存页大小这两个变量在初始化内存时会被经常用到。 func osinit() {ncpu getncpu()physPageSize getPageSize() }schedinit 在获取了 CPU 和 内存的必要参数后Go 开始调用 schedinit 初始化包括 内存分配器Goroutine 调度器垃圾回收器等语言层面的组件。 函数开始会对用到的锁调用 lockInit但除非你设置 GOEXPERIMENTstaticlockranking 开启 static lock ranking 功能否则他们都是空函数不会做任何事情static lock ranking 的作用是在运行时 mutex 出现死锁时报告错误具体可以参考提交 comment 信息. 这里大部分是一些初始化函数用来初始化堆栈分配gc 等模块的关键数据结构值得注意的有下面几个地方 这里设置了 sched.maxmcount1000 在 mcommoninit 中将当前 m 即 m0 的 id 设置成了 -1 mcommoninit(_g_.m, -1)并且为其新初始化了一个 g 挂在 gsignal 上实际上这个 groutine 是专门用来处理操作系统信号的有意思的是这个 g 的栈空间是 32k 大于普通 g 的 8k func mpreinit(mp *m) {mp.gsignal malg(32 * 1024) // OS X wants 8Kmp.gsignal.m mp}mcommoninit 还将当前 m 插入了全局 m 链表 allm 的表头事实上每个新建的 m 都会被加入到这个链表的头部每个 m 有一个 alllink 字段指向该链表注意现在还在初始化allm 本身还是 nil所以这里直接将 m0 原子地赋值给了 allm mp.alllink allmatomicstorep(unsafe.Pointer(allm), unsafe.Pointer(mp))调用 goargs 和 goenvs 初始化命令后参数和环境变量我们在上一节中已经讲过了 procresize 中根据 GOMAXPROCS 和 cpu 数量初始化所有 p procs : ncpuif n, ok : atoi32(gogetenv(GOMAXPROCS)); ok n 0 {procs n}if procresize(procs) ! nil {throw(unknown runnable goroutine during bootstrap)}这里会根据 GOMAXPROCS 和 cpu 数量 直接将 nprocs 个 p 全部初始化好并放入 allp 列表中并将 m0 和第 0 个 p我们称为 p0绑定 _g_.m.p 0p : allp[0]p.m 0p.status _Pidleacquirep(p)func acquirep(_p_ *p) {wirep(_p_)// ...}func wirep(_p_ *p) {_g_ : getg()// ..._g_.m.p.set(_p_)_p_.m.set(_g_.m)_p_.status _Prunning}可以留意一下 p 的状态流转 在刚被初始化好还没有与 m 绑定时状态是 _Pidle 绑定后状态流转为 _Prunning 最后所有空闲的 p现在是 除 p0 外的所有p放入 _Pidle 列表 var runnablePs *pfor i : nprocs - 1; i 0; i-- {p : allp[i]if _g_.m.p.ptr() p {continue}p.status _Pidleif runqempty(p) {pidleput(p) // 加入 _Pidle list } else {p.m.set(mget())p.link.set(runnablePs)runnablePs p}}总结一下这一步主要是初始化了一些关键数据结构的字段除此之外最重要的就是将 m0 加入了全局 m 链表并切初始化了所有 p 并实现了 m0 和 p0 的绑定至此 g0 m0 p0 全部如下图绑定和初始化结束下一步就是启动新的 groutine 去执行 main 函数了 newproc // create a new goroutine to start program MOVD $runtime·mainPC(SB), R0 // entry SUB $16, RSP MOVD R0, 8(RSP) // arg MOVD $0, 0(RSP) // dummy LR BL runtime·newproc(SB) ADD $16, RSP这一步的作用是通过 newproc 创建一个新的 goroutine 去执行 main 函数, 它接受一个函数指针作为参数 func newproc(fn *funcval) {gp : getg() // 这里是 g0pc : getcallerpc()systemstack(func() {newg : newproc1(fn, gp, pc)pp : getg().m.p.ptr() // p0runqput(pp, newg, true)if mainStarted {wakep()}}) }systemstack 用于切换到系统栈执行有关系统栈signal 和用户栈可以参考下面 细节/stack 一节我们这里本来就在系统栈上. newproc1 创建一个新的 g 细节 为了逻辑通顺上面有些细节没有介绍放在了这里 stack https://go.dev/src/runtime/HACKING Every non-dead G has a user stack associated with it, which is what user Go code executes on. User stacks start small (e.g., 2K) and grow or shrink dynamically. 每个非死亡的 Ggoroutine都关联有一个用户栈user stack这是用户Go代码执行的地方。用户栈起始时很小例如2KB并且可以动态增长或缩小。 Every M has a system stack associated with it (also known as the M’s “g0” stack because it’s implemented as a stub G) and, on Unix platforms, a signal stack (also known as the M’s “gsignal” stack). System and signal stacks cannot grow, but are large enough to execute runtime and cgo code (8K in a pure Go binary; system-allocated in a cgo binary). 每个MOS线程都关联有一个系统栈system stack也被称为M的“g0”栈因为它是作为一个存根Gstub G实现的。在Unix平台上M还有一个信号栈signal stack也被称为M的“gsignal”栈。系统栈和信号栈不能增长但足够大以执行运行时和cgo代码在纯Go二进制文件中为8KB在cgo二进制文件中由系统分配。 Runtime code often temporarily switches to the system stack using systemstack, mcall, or asmcgocall to perform tasks that must not be preempted, that must not grow the user stack, or that switch user goroutines. Code running on the system stack is implicitly non-preemptible and the garbage collector does not scan system stacks. While running on the system stack, the current user stack is not used for execution. 运行时代码经常使用 systemstack、mcall或asmcgocall临时切换到系统栈来执行不能被抢占的任务或者不能增长用户栈的任务或者需要切换用户goroutine的任务。在系统栈上运行的代码隐式地不可抢占垃圾回收器不会扫描系统栈。在系统栈上运行时当前的用户栈不用于执行。 文档描述的很清楚在处理一些特殊任务时需要切换到系统栈这里我们简单看一下 systemstack 的实现这是一个汇编函数 参考 记录 getg 是一个伪指令 https://groups.google.com/g/golang-nuts/c/KgPOzaMylHo/m/zX0GosvnAQAJ 所有的 m 通过 m.alllink 连了一个链表有一个 allm 指针指向这个链表的头部每次创建新的 m 都会插入到链表头部 https://pubs.opengroup.org/onlinepubs/009695399/functions/pthread_key_create.html ↩︎ ↩︎
http://www.pierceye.com/news/668060/

相关文章:

  • 在线网站建设费用是多少大学生活动策划书模板
  • 动物网站建设wordpress无法跳转正确页面
  • 上海市建设工程 安全协会网站wordpress会员微信支付宝
  • pc网站转换手机网站代码桂林工作网招聘
  • 营销型网站建设的要素怎么建网站赚钱
  • 成都网站建设学习郑州制作网站推荐
  • 网站建设 镇江丹阳php网站开发实例教程代码
  • 佛山外贸网站建设方案专业网站建设系统
  • 做一个网站团队需要哪些人员花钱也可以哪些网站可以做推广广告
  • 各省施工备案网站做动漫网站的素材
  • 新余网站设计网站模板做网站
  • 防止服务器上的网站被进攻app推广兼职
  • 保定电商网站建设国内最好的crm软件
  • 企业网站建设哪家公司好莱芜金点子信息港房产网
  • 个人可以建设网站吗海淀网站建设本溪
  • 宜昌网站建设兼职怎样做自媒体拍视频赚钱
  • 我的世界做视频封面的网站免费的app源码网
  • 网站搭建wordpress参考消息电子版在线阅读
  • 成立一个网站平台要多少钱科技有限公司一般是做什么的
  • 邵阳 网站开发 招聘桂林阳朔楼盘最新价格
  • 如何建设网站导航内链接wordpress 特别慢
  • 蚌埠网站建设文章网站软件定制开发公司
  • 软件通网站建设百度收录网站电话
  • 取消网站备案制度正规的电商平台有哪些
  • 茶叶网站源码php6731官方网站下载
  • 网站建设mfdos 优帮云制作简历哪个网站好
  • 淮南市城乡建设局网站网站seo收费
  • 陕西手机网站制作描述网站开发的广告词
  • 一个网址建多个网站手机网站数据加载
  • 网站视觉分析上海做saas平台网站的公司