Technology

Chart Type 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go 内存管理 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

Architecture

实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 在离线业务混部 RNN pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF 生命周期管理 openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Kubernetes webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 概率论 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列


汇编语言

2021年03月07日

简介

在计算机中,并不支持直接将数据在不同的内存之间传送,更不支持将数据从内存 直接传送到外部设备,例如磁盘或网络端口。cpu 唯一支持不同部件之间的直接数据传送只有寄存器到寄存器了。由于高级编程语言并不能直接操作寄存器,所有的数据传送指令以及针对寄存器的读写指令都被封装为面向变量的编程,而变量的存储介质是内存,因此高级编程语言中所有数据传送指令都必须经过寄存器的中转。PS:在 x86 指令集中,受限于 CPU 实现的复杂度,不存在可以将两个内存地址同时作为 src 和 dest 参数的指令。

与机器对话——汇编语言基础

计算机的处理器有很多不同的架构,比如 x86-64、ARM、Power 等,每种处理器的指令集都不相同,那也就意味着汇编语言不同,不具备可移植性,汇编语言在机器指令之上基本不具有任何抽象,使用助记符(Mnemonic)来表示每个低级的机器指令。

本文以x86-64 架构为例

#include <stdio.h>
int main(int argc, char* argv[]){
    printf("Hello %s!\n", "Richard");
    return 0;
}

对应的汇编代码

.section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 14    sdk_version 10, 14
    .globl  _main                   ## -- Begin function main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    leaq    L_.str(%rip), %rdi
    leaq    L_.str.1(%rip), %rsi
    xorl    %eax, %eax
    callq   _printf
    xorl    %eax, %eax
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function 
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz  "Hello %s!\n"

L_.str.1:                               ## @.str.1
    .asciz  "Richard"

.subsections_via_symbols

这段代码里有指令、伪指令、标签和注释四种元素,每个元素单独占一行。

  1. 伪指令以“.”开头,末尾没有冒号“:”。伪指令是是辅助性的,不是真正的 CPU 指令,就是写给汇编器的,汇编器在生成目标文件时会用到这些信息。
  2. 标签以冒号“:”结尾,用于对伪指令生成的数据或指令做标记。可以代表一段代码或者常量的地址,其他代码可以访问标签。可一开始,我们没法知道这个地址的具体值,必须生成目标文件后,才能算出来。所以,标签会简化汇编代码的编写。
  3. 注释,以“#”号开头,这跟 C 语言中以 // 表示注释语句是一样的。

在代码中,助记符“movq”“xorl”中的“mov”和“xor”是指令,而“q”和“l”叫做后缀,表示操作数的位数。后缀一共有 b, w, l, q 四种,分别代表 8 位、16 位、32 位和 64 位。在指令中使用操作数,可以使用四种格式,它们分别是:立即数、寄存器、直接内存访问和间接内存访问。

操作数格式 示例 含义 指令除立即数外都是基于地址的
立即数以$开头 movl $40, %eax 把 40 这个数字拷贝到 %eax 寄存器  
直接内存访问 callq _printf 调用printf函数 操作数是内存地址
_printf是一个函数入口的地址,
汇编器帮我们计算出程序装载在内存时,每个字面量和过程的地址
寄存器 subq $8, %rsp 把%rsp的值减8 寄存器本身既是存储,也是存储地址
间接内存访问带有括号 movl $10, (%rsp) %rsp 寄存器的值所指向的地址 对应的内存设为10 寄存器的值是内存地址

汇编代码其实比较简单,它所做的工作不外乎就是把数据在内存和寄存器中搬来搬去或做一些基础的数学和逻辑运算

过程调用和栈帧

/*function-call1.c */
#include <stdio.h>
int fun1(int a, int b){
    int c = 10;
    return a+b+c;
}
int main(int argc, char *argv[]){
    printf("fun1: %d\n", fun1(1,2));
    return 0;
} 

等价的汇编代码如下

# function-call1-craft.s 函数调用和参数传递
    # 文本段,纯代码
    .section    __TEXT,__text,regular,pure_instructions

_fun1:
    # 函数调用的序曲,设置栈指针
    pushq   %rbp           # 把调用者的栈帧底部地址保存起来   
    movq    %rsp, %rbp     # 把调用者的栈帧顶部地址,设置为本栈帧的底部

    subq    $4, %rsp       # 扩展栈

    movl    $10, -4(%rbp)  # 变量c赋值为10,也可以写成 movl $10, (%rsp)

    # 做加法
    movl    %edi, %eax     # 第一个参数放进%eax
    addl    %esi, %eax     # 把第二个参数加到%eax,%eax同时也是存放返回值的寄存器
    addl    -4(%rbp), %eax # 加上c的值

    addq    $4, %rsp       # 缩小栈

    # 函数调用的尾声,恢复栈指针为原来的值
    popq    %rbp           # 恢复调用者栈帧的底部数值
    retq                   # 返回

    .globl  _main          # .global伪指令让_main函数外部可见
_main:                                  ## @main
    
    # 函数调用的序曲,设置栈指针
    pushq   %rbp           # 把调用者的栈帧底部地址保存起来  
    movq    %rsp, %rbp     # 把调用者的栈帧顶部地址,设置为本栈帧的底部
    
    # 设置第一个和第二个参数,分别为1和2
    movl    $1, %edi
    movl    $2, %esi

    callq   _fun1                # 调用函数

    # 为pritf设置参数
    leaq    L_.str(%rip), %rdi   # 第一个参数是字符串的地址
    movl    %eax, %esi           # 第二个参数是前一个参数的返回值

    callq   _printf              # 调用函数

    # 设置返回值。这句也常用 xorl %esi, %esi 这样的指令,都是置为零
    movl    $0, %eax
    
    # 函数调用的尾声,恢复栈指针为原来的值
    popq    %rbp         # 恢复调用者栈帧的底部数值
    retq                 # 返回

    # 文本段,保存字符串字面量                                  
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz  "Hello World! :%d \n"

C函数翻译成汇编代码,有这样的固定结构。就好像java的 synchronized 关键字 自动有entermonitor 和 exitmonitor 一样。

# 函数调用的序曲,设置栈指针
pushq  %rbp        # 把调用者的栈帧底部地址保存起来  
movq  %rsp, %rbp   # 把调用者的栈帧顶部地址,设置为本栈帧的底部

...

# 函数调用的尾声,恢复栈指针为原来的值
popq  %rbp         # 恢复调用者栈帧的底部数值
函数调用涉及指令 等价指令 备注
callq _fun1 pushq %rip
jmp _fun1
保存下一条指令的地址,用于函数返回继续执行
跳转到函数_fun1
pushq %rbp subq $8, %rsp
movq %rbp, (%rsp)
把%rsp的值减8,也就是栈增长8个字节,从高地址向低地址增长
把%rbp的值写到当前栈顶指示的内存位置
popq %rbp movq (%rsp), %rbp
addq $8, %rsp
把栈顶位置的值恢复回%rbp,这是之前保存在栈里的值
把%rsp的值加8,也就是栈减少8个字节
retq popq %rip
jmp %rip
恢复指令指针寄存器

pushq %rbp 执行后情况

  1. pushq 和 popq 虽然是单“参数”指令,但一个隐藏的“参数”就是 %rsp。
  2. 通过移动 %rsp 指针来改变帧的大小。%rbp 和 %rsp 之间的空间就是当前栈帧。
  3. 栈帧先进后出 (一个函数的相关 信息占用一帧)。或者栈帧二字 重点在帧上。%rbp 在函数调用时一次移动 一个栈帧的大小,%rbp在整个函数执行期间是不变的。使用 %rbp 前,要先保护起来,在栈帧内存一个备份。
  4. 函数内部访问 栈帧 可以使用 -4(%rbp)表示地址,表示%rbp 寄存器存储的地址减去4的地址。说白了,栈帧内可以基于 (%rbp) 随机访问+4(%rsp)效果类似。
  5. %rsp并不总是指向真实的栈顶:在 X86-64 架构下,新的规范让程序可以访问栈顶之外 128 字节的内存,所以,我们甚至不需要通过改变 %rsp 来分配栈空间,而是直接用栈顶之外的空间。比如栈帧大小是16,即·(%rbp)-(%rsp) = 16,可以在(只能在叶子函数的)代码中直接使用 内存地址-32(%rbp)。叶子函数是functions which do not call any other function.
  6. 除了callq/pushq/popq/retq 指令操作%rsp外,函数执行期间,可以mov (%rsp)使其指向栈顶一步到位,(%rsp)也可以和(%rbp)挨着一步不动,也可以随着变量的分配慢慢移动。
  7. 函数调用 使用栈空间的大小 是编译时就可以确定的,但不是说编译时就分配好栈空间了,栈空间运行时动态分配与回收。这与全局变量、常量 编译时就确定好内存分配是不同的。函数返回时,栈顶指针重新赋值了,栈顶外的内存就抛弃了,这就是所谓的栈里的内存可以自动管理(在编译器和操作系统的配合下)。所以说栈里声明的本地变量,它的生存期跟作用域是一致的。

循环语句和 if 语句的秘密在于比较指令和有条件跳转指令(jmp是无条件跳转指令),它们都用到了 EFLAGS 寄存器。

在 x86-64 的机器指令中,函数调用是通过 call 指令来完成。而每一个函数体在执行完毕后,都需要再通过 ret 指令来退出函数的执行,并转移代码执行流程到之前函数调用指令的下一条指令上。PS:函数调用一般要符合ABI 规范。

  1. call 和 ret 实现 跳转与返回
  2. 函数执行有上下文(这也是函数调用 与 JUMP 指令的区别),包括通用、专用寄存器 和 栈,尤其是栈,函数的切换伴随着 栈(栈寄存器)的切换
    1. 参数,入参 在call 之前写入约定通用寄存器,返回值返回之前 写入约定通用寄存器。入参 比较多时 多余的参数压入栈
    2. 栈,call 和 ret 会用到 栈寄存器 及读写栈
  3. 因为单条指令实现复杂度的问题,call 和 ret 的复杂度有限,call/ret=跳转+一两条指令,函数切换涉及的参数、栈等大部分操作 还是要在函数调用前、函数头部、返回尾部执行很多条配合的指令。