从汇编角度看fmt.Println是如何系统调用的

XITEHIP · · 868 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

fmt.Println是打印到终端字符串,那就涉及到golang的系统调用了,系统调用的底层是通过汇编的系统调用来完成的,下面就去看一下具体的调用过程。

以下代码环境:
1,Linux version 3.10.0-957.12.2.el7.x86_64
2,go version go1.12.5 linux/amd64

#main

package main
import "fmt"
func main() {
    fmt.Println("hello world!")
}

跟踪函数调用发现最终会调用Syscall函数,见下方代码:

#syscall/syscall_unix.go

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

这4个函数没有函数体,它们的是通过汇编实现的,其中Syscall是这里Println的底层函数,其参数总共有4个,trap表示系统调用标识号(linux amd64中为1),a1表示标准输出(Stdout = 1)标识号,a2表示字符串内存地址,a3表示字符串长度。以下是汇编实现:

#syscall/asm_linux_amd64.s

TEXT    ·Syscall(SB),NOSPLIT,$0-56
#1,  通知runtime调度器让出运行时间
    CALL    runtime·entersyscall(SB)

 #2,   获取内存中的调用参数并按约定传递给寄存器)
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    $0, R10
    MOVQ    $0, R8
    MOVQ    $0, R9
    MOVQ    trap+0(FP), AX  // syscall entry

#3,通知内核系统(amd64)调用
    SYSCALL

#4,判断系统调用的执行结果,并进行跳转到ok处
    CMPQ    AX, $0xfffffffffffff001
    JLS ok

#5-1,执行失败,置空返回值
    MOVQ    $-1, r1+32(FP)
    MOVQ    $0, r2+40(FP)
    NEGQ    AX
    MOVQ    AX, err+48(FP)
  
#6-1,恢复goroutine的运行并返回
    CALL    runtime·exitsyscall(SB)
    RET

 #5-2,执行成功,拷贝执行结果到返回值
ok:
    MOVQ    AX, r1+32(FP)
    MOVQ    DX, r2+40(FP)
    MOVQ    $0, err+48(FP)

#6-2,恢复goroutine的运行并返回
    CALL    runtime·exitsyscall(SB)
    RET

以下是系统调用sys_write的说明:

///usr/src/kernels/3.10.0-957.12.2.el7.x86_64/include/linux/syscalls.h
//fd标准输入输出,buf输出字符串地址,count字符串长度
asmlinkage long sys_write(unsigned int fd, const char __user *buf,size_t count);

或者通过man 2 write查看:


图片.png

下面通过将golang源码直接编译成go格式的汇编代码(截取一部分)如下图:

go tool compile -S print.go >> print.s

BAB7B8A2-9736-424C-8DCC-93D73E882CDE.png

根据上图可以发现,CALL fmt.Fprintln(SB)之前会把标准输出地址和字符串地址压如栈中为后续获取这2个参数做准备。然后用gdb调试工具去去查看一下fmt.Fprintln(SB)的具体实现。
1,go build -o print 生成可执行文件
2,gdb print
3,b main.main 设置断点
4, layout split 显示源代码和反汇编窗口
5,r,s 运行并分步执行查看调用过程及寄存器,内存等信息。
最终见下2幅图:


D5ACDD6A-125B-4D46-B15B-57C5BA72D2A7.png

85374CB5-0B1D-4700-8C05-B024FA205E36.png

第一幅图黄框处是系统调用的汇编代码,可以和golang源码汇编文件作对比看有什么区别。
第二幅图是执行syscall之前相关寄存器和内存的数据。rdi=1代表amd64架构下 系统调用号是1(可以从/usr/include/asm/unistd_64.h 处查到)。rsi=0xc0000140c0表示字符串的地址,红色箭头黄框处就是此地址指向的内存中的数据(10进制数表示),根据ascii表查找104=h 101=e....确实是"hello world\n"。

系统调用时如果调用成功,会把输出长度返回给ax

EB425192-CB39-467A-A6A2-6765BA6EFFE4.png
FC717847-FA38-4747-A7C5-2537EB819F17.png

参考:
系统调用在 Golang 中的实践


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:XITEHIP

查看原文:从汇编角度看fmt.Println是如何系统调用的

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

868 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传