Golang反射机制的实现分析——reflect.Type方法查找和调用
在《Golang反射机制的实现分析——reflect.Type类型名称》一文中,我们分析了Golang获取类型基本信息的流程。本文将基于上述知识和经验,分析方法的查找和调用。(转载请指明出于breaksoftware的csdn博客)
方法
package mainimport ("fmt""reflect"
)type t20190107 struct {v int
}func (t t20190107) F() int {return t.v
}func main() {i := t20190107{678}t := reflect.TypeOf(i)for it := 0; it < t.NumMethod(); it++ {fmt.Println(t.Method(it).Name)}f, _ := t.MethodByName("F")fmt.Println(f.Name)r := f.Func.Call([]reflect.Value{reflect.ValueOf(i)})[0].Int()fmt.Println(r)
}
这段代码,我们构建了一个实现了F()方法的结构体。然后使用反射机制,通过遍历和名称查找方式,找到方法并调用它。
调用reflect.TypeOf之前的逻辑,我们已经在上节中讲解了。本文不再赘述。
0x00000000004b0226 <+134>: callq 0x491150 <reflect.TypeOf>0x00000000004b022b <+139>: mov 0x18(%rsp),%rax0x00000000004b0230 <+144>: mov 0x10(%rsp),%rcx……0x00000000004b026a <+202>: mov 0xe0(%rsp),%rax0x00000000004b0272 <+210>: mov 0xd8(%rax),%rax0x00000000004b0279 <+217>: mov 0xe8(%rsp),%rcx0x00000000004b0281 <+225>: mov %rcx,(%rsp)0x00000000004b0285 <+229>: callq *%rax0x00000000004b0287 <+231>: mov 0x8(%rsp),%rax0x00000000004b028c <+236>: mov %rax,0x90(%rsp)0x00000000004b0294 <+244>: mov 0x78(%rsp),%rcx0x00000000004b0299 <+249>: cmp %rax,%rcx
这段逻辑对应于上面go代码中的第19行for循环逻辑。
汇编代码的第9行,调用了一个保存于寄存器中的地址。依据之前的分析经验,这个地址是rtype.NumMethod()方法地址。
(gdb) disassemble $rax
Dump of assembler code for function reflect.(*rtype).NumMethod:
看下Golang的代码,可以发现其区分了类型是否是“接口”。“接口”类型的计算比较特殊,而其他类型则调用rtype.exportedMethods()方法。
func (t *rtype) NumMethod() int {if t.Kind() == Interface {tt := (*interfaceType)(unsafe.Pointer(t))return tt.NumMethod()}if t.tflag&tflagUncommon == 0 {return 0 // avoid methodCache synchronization}return len(t.exportedMethods())
}
因为我们这个例子是struct类型,所以调用的是下面的方法
var methodCache sync.Map // map[*rtype][]methodfunc (t *rtype) exportedMethods() []method {methodsi, found := methodCache.Load(t)if found {return methodsi.([]method)}
methodCache是个全局变量,它以rtype为key,保存了其对应的方法信息。这个缓存在初始时没有数据,所以我们第一次对某rtype调用该方法,是找不到其对应的缓存的。
ut := t.uncommon()if ut == nil {return nil}
rtype.uncommon()根据变量类型,在内存中寻找uncommonType信息。
func (t *rtype) uncommon() *uncommonType {if t.tflag&tflagUncommon == 0 {return nil}switch t.Kind() {case Struct:return &(*structTypeUncommon)(unsafe.Pointer(t)).ucase Ptr:……}
}
这段逻辑,我们只要看下汇编将该地址如何转换的
0x000000000048d4df <+143>: cmp $0x19,%rcx0x000000000048d4e3 <+147>: jne 0x48d481 <reflect.(*rtype).uncommon+49>0x000000000048d4e5 <+149>: add $0x50,%rax0x000000000048d4e9 <+153>: mov %rax,0x10(%rsp)0x000000000048d4ee <+158>: retq
rax寄存器之前保存的是rtype的地址0x4d1320,于是uncommonType的信息保存于0x4d1320+0x50位置。
type uncommonType struct {pkgPath nameOff // import path; empty for built-in types like int, stringmcount uint16 // number of methods_ uint16 // unusedmoff uint32 // offset from this uncommontype to [mcount]method_ uint32 // unused
}
依据其结构体,我们可以得出各个变量的值:mcount=0x1,moff=0x28。此处mcount的值正是测试结构体的方法个数1。
获取完uncommonType信息,我们需要通过其找到方法信息
allm := ut.methods()
func (t *uncommonType) methods() []method {return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff)))[:t.mcount:t.mcount]
}
这个计算比较简单,只是在uncommonType的地址0x4d1370基础上偏移t.moff=0x28即可。我们查看下其内存
(gdb) x/16xb 0x4d1370+0x28
0x4d1398: 0x07 0x00 0x00 0x00 0x40 0x18 0x01 0x00
0x4d13a0: 0x80 0xf8 0x0a 0x00 0x80 0xf1 0x0a 0x00
// Method on non-interface type
type method struct {name nameOff // name of methodmtyp typeOff // method type (without receiver)ifn textOff // fn used in interface call (one-word receiver)tfn textOff // fn used for normal method call
}
和method结构对应上就是method{nameOff=0x07, typeOff=0x011840, ifn=0x0af880, tfn=0x0af180}。
获取方法信息后,exportedMethods筛选出可以对外访问的方法,然后将结果保存到methodCache中。这样下次就不用再找一遍了。
……methodsi, _ = methodCache.LoadOrStore(t, methods)return methodsi.([]method)
}
获取到方法个数后,我们就可以使用rtype.Method()方法获取方法信息了。和其他rtype方法一样,Method也是通过指针偏移算出来的。
0x00000000004b02a3 <+259>: mov 0xe0(%rsp),%rax0x00000000004b02ab <+267>: mov 0xb0(%rax),%rax0x00000000004b02b2 <+274>: mov 0x78(%rsp),%rcx0x00000000004b02b7 <+279>: mov 0xe8(%rsp),%rdx0x00000000004b02bf <+287>: mov %rcx,0x8(%rsp)0x00000000004b02c4 <+292>: mov %rdx,(%rsp)0x00000000004b02c8 <+296>: callq *%rax
func (t *rtype) Method(i int) (m Method) {if t.Kind() == Interface {tt := (*interfaceType)(unsafe.Pointer(t))return tt.Method(i)}methods := t.exportedMethods()if i < 0 || i >= len(methods) {panic("reflect: Method index out of range")}p := methods[i]pname := t.nameOff(p.name)m.Name = pname.name()fl := flag(Func)mtyp := t.typeOff(p.mtyp)ft := (*funcType)(unsafe.Pointer(mtyp))in := make([]Type, 0, 1+len(ft.in()))in = append(in, t)for _, arg := range ft.in() {in = append(in, arg)}out := make([]Type, 0, len(ft.out()))for _, ret := range ft.out() {out = append(out, ret)}mt := FuncOf(in, out, ft.IsVariadic())m.Type = mttfn := t.textOff(p.tfn)fn := unsafe.Pointer(&tfn)m.Func = Value{mt.(*rtype), fn, fl}m.Index = ireturn m
}
Method方法构建了一个Method结构体,其中方法名称、入参、出参等都不再分析。我们关注下函数地址的获取,即第27行。
textOff底层调用的是
func (t *_type) textOff(off textOff) unsafe.Pointer {base := uintptr(unsafe.Pointer(t))var md *moduledatafor next := &firstmoduledata; next != nil; next = next.next {if base >= next.types && base < next.etypes {md = nextbreak}}if md == nil {reflectOffsLock()res := reflectOffs.m[int32(off)]reflectOffsUnlock()if res == nil {println("runtime: textOff", hex(off), "base", hex(base), "not in ranges:")for next := &firstmoduledata; next != nil; next = next.next {println("\ttypes", hex(next.types), "etypes", hex(next.etypes))}throw("runtime: text offset base pointer out of range")}return res}res := uintptr(0)// The text, or instruction stream is generated as one large buffer. The off (offset) for a method is// its offset within this buffer. If the total text size gets too large, there can be issues on platforms like ppc64 if// the target of calls are too far for the call instruction. To resolve the large text issue, the text is split// into multiple text sections to allow the linker to generate long calls when necessary. When this happens, the vaddr// for each text section is set to its offset within the text. Each method's offset is compared against the section// vaddrs and sizes to determine the containing section. Then the section relative offset is added to the section's// relocated baseaddr to compute the method addess.if len(md.textsectmap) > 1 {for i := range md.textsectmap {sectaddr := md.textsectmap[i].vaddrsectlen := md.textsectmap[i].lengthif uintptr(off) >= sectaddr && uintptr(off) <= sectaddr+sectlen {res = md.textsectmap[i].baseaddr + uintptr(off) - uintptr(md.textsectmap[i].vaddr)break}}} else {// single text sectionres = md.text + uintptr(off)}if res > md.etext {println("runtime: textOff", hex(off), "out of range", hex(md.text), "-", hex(md.etext))throw("runtime: text offset out of range")}return unsafe.Pointer(res)
}
我们又看到模块信息了,这在《Golang反射机制的实现分析——reflect.Type类型名称》一文中也介绍过。
通过rtype的地址确定哪个模块,然后查看模块的代码块信息。
第33行显示,如果该模块中的代码块多于1个,则通过偏移量查找其所处的代码块,然后通过虚拟地址的偏移差算出代码的真实地址。
如果代码块只有一个,则只要把模块中text字段表示的代码块起始地址加上偏移量即可。
在我们的例子中,只有一个代码块。所以使用下面的方式。
之前我们通过内存分析的偏移量tfn=0x0af180,而此模块记录的代码块起始地址是0x401000。则反汇编这块地址
(gdb) disassemble 0x401000+0x0af180
Dump of assembler code for function main.t20190107.F:0x00000000004b0180 <+0>: movq $0x0,0x10(%rsp)0x00000000004b0189 <+9>: mov 0x8(%rsp),%rax0x00000000004b018e <+14>: mov %rax,0x10(%rsp)0x00000000004b0193 <+19>: retq
如此我们便取到了函数地址。
rtype.MethodByName方法实现比较简单,它只是遍历并通过函数名匹配方法信息,然后返回
func (t *rtype) MethodByName(name string) (m Method, ok bool) {if t.Kind() == Interface {tt := (*interfaceType)(unsafe.Pointer(t))return tt.MethodByName(name)}ut := t.uncommon()if ut == nil {return Method{}, false}utmethods := ut.methods()for i := 0; i < int(ut.mcount); i++ {p := utmethods[i]pname := t.nameOff(p.name)if pname.isExported() && pname.name() == name {return t.Method(i), true}}return Method{}, false
}
反射出来的函数使用Call方法调用。其底层就是调用上面确定的函数地址。
func (v Value) Call(in []Value) []Value {v.mustBe(Func)v.mustBeExported()return v.call("Call", in)
}func (v Value) call(op string, in []Value) []Value {// Get function pointer, type.……if v.flag&flagMethod != 0 {rcvr = vrcvrtype, t, fn = methodReceiver(op, v, int(v.flag)>>flagMethodShift)} else if v.flag&flagIndir != 0 {fn = *(*unsafe.Pointer)(v.ptr)} else {fn = v.ptr}……// Call.call(frametype, fn, args, uint32(frametype.size), uint32(retOffset))……
}
总结
- 通过rtype中的kind信息确定保存方法信息的偏移量。
- 相对于rtype起始地址,使用上面偏移量获取方法信息组。
- 通过方法信息中的偏移量和模块信息中记录的代码块起始地址,确定方法的地址。
- 通过反射调用方法比直接调用方法要复杂很多
相关文章:
太狠!33岁年薪50万:“复工第一天,谢谢裁掉我!” 网友:有底气!
最近脉脉一则帖子炸锅了:某HR发帖称公司以按时下班为由裁员。这种情况下很多人都慌了,大家纷纷把“副业救国”奉为神律。可是你有没有认真的想过,为什么现在大家都需要副业:意外裁员后,房贷能够按时还上不至于“回收”…

SEO内部链接优化的技巧
内部链接是搜索引擎优化中的重要因素之一。思亿欧做的SEO调查发现,国内大部分网站都没有怎么做内部链接优化。这可能是网站管理员并不知晓SEO或者是对内部链接优化不够重视。 内部链接的设计不能是单纯的为了SEO的目的而作内部链接,同时要注意规划一个良…

Ubuntu 15.10安装ns2.35+nam
2019独角兽企业重金招聘Python工程师标准>>> Step1: 更新系统sudo apt-get update #更新源列表sudo apt-get upgrade #更新已经安装的包sudo apt-get dist-upgrade #更新软件,升级系统Step2:安装ns2需要的几个包sudo apt-get install build-essentialsu…

bug诞生记——不定长参数隐藏的类型问题
这个bug的诞生源于项目中使用了一个开源C库。由于对该C库API不熟悉,一个不起眼的错误调用,导致一系列诡异的问题。最终经过调试,我们发现发生了内存覆盖问题。为了直达问题根节,我将问题代码简化如下(转载请指明出于br…

yahoo註冊.com 域名1.99$/年
yahoo註冊.com 域名1.99$/年趕快去註冊吧http://order.sbs.yahoo.com/ds/reviewplanoption?.pYD1&mdom&.srcsbs&.promoBESTDEAL&dzzhen an.com支持paypal付款一個yahoo帳戶只能註冊一個如果覺得續費比較貴,可在註冊兩個月後轉出到godaddy.转载于:h…
Excel弱爆了!这个工具30分钟完成了我一天的工作量,零基础、文科生也能学!...
在大数据浪潮当中,数据分析是这个时代的不二“掘金技能”。我们每一个人,每天无时无刻都在生产数据,一分钟内,微博上新发的数据量超过10万,b站的视频播放量超过600万......这些庞大的数字,意味着什么&#…

Myeclipse快捷键的使用
存盘 Ctrls(肯定知道) 注释代码 Ctrl/ 取消注释 Ctrl\(Eclipse3已经都合并到Ctrl/了) 代码辅助 Alt/ 快速修复 Ctrl1 代码格式化 CtrlShiftf 整理导入 CtrlShifto 切换窗口 Ctrlf6 <可改为ctrltab方便> ctrlshiftM 导入未引用的包 ctrlw 关闭单个窗口 F3 跳转到类、变量的…

PL/SQL三种集合类型的比较
PL/SQL三种集合类型的比较<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />集合是指在一个程序变量中包含多个值。PL/SQL提供的集合类型如下:Associative Array:TYPE t IS TABLE OF something INDEX BY PLS_INTEGER;N…
夺得WSDM Cup 2020大赛金牌的这份参赛方案,速来get!
近日,在美国休斯敦闭幕的第13届网络搜索与数据挖掘国际会议(WSDM 2020)上,华为云语音语义创新Lab带领的联合团队,摘得WSDM Cup 2020大赛“论文引用意图识别任务”金牌(Gold Medal)。WSDM被誉为全…

bug诞生记——信号(signal)处理导致死锁
这个bug源于项目中一个诡异的现象:代码层面没有明显的锁的问题,但是执行时发生了死锁一样的表现。我把业务逻辑简化为:父进程一直维持一个子进程。(转载请指明出于breaksoftware的csdn博客) 首先我们定义一个结构体Pro…

Linux下SVN服务器支持Apache的http和svnserve独立服务器
2019独角兽企业重金招聘Python工程师标准>>> 说明 服务器操作系统:CentOS 6.6 关闭防火墙,SElinux 实现 1、在服务器上安装配置SVN服务; 2、SVN服务支持svnserve独立服务模式访问; 3、SVN服务支持Apache的http模式访问…

AWS攻略——使用CodeCommit托管代码
除了我们熟悉的github,各大云厂商也有自己的代码托管服务。本文讲解如何在Amazon的CodeCommit中托管代码。(转载请指明出于breaksoftware的csdn博客) 根账户登录 AWS有两种账户登录界面。 IAM账户登录界面 根账户登录界面我们先使用根…

使用alterMIME实现添加message footer功能
1. 安装alterMIME tar zxvf altermime-0.3.8.tar.gz cd altermin3-0.3.8 make make install altermine将被编译安装到/usr/local/bin/2. 使用必备条件:一个运行且配置正常的邮件服务器3. 配置AlterMIME3.1 为altermine创建一个系统帐号,如下&#x…
Facebook最新研究:无需额外训练AI,即可加速NLP任务
作者 | KYLE WIGGERS译者 | Kolen出品 | AI科技大本营(ID:rgznai100)自然语言模型通常要解决两个难题:将句子前缀映射到固定大小的表示形式,并使用这些表示形式来预测文本中的下一个单词。在最近的一篇论文(https://ar…
PgSQL · 特性分析 · full page write 机制
PG默认每个page的大小为8K,PG数据页写入是以page为单位,但是在断电等情况下,操作系统往往不能保证单个page原子地写入磁盘,这样就极有可能导致部分数据块只写到4K(操作系统是一般以4K为单位),这些“部分写”的页面包含…

局域网DVD yum源的制作
今天在网上溜达,看到这篇文章不错,于是就转载过来,感谢原作者的辛苦劳动.源地址:http://blog.chinaunix.net/u3/94782/showart_1953260.html一:两台计算机做实验<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />1&…

AWS攻略——使用S3托管静态网页
在AWS上有很多部署静态网页的方式,比如使用EC2或者Lightsail。但是不管使用上述哪种方案,都需要预先部署如Nignx或者Apache等Http服务。这对纯前端同学来说可能有点复杂,而AWS提供了更简单的部署方式——只需要提供静态网页文件的“S3网页托管…
2020年涨薪26-30%,能实现吗?18%数据科学家是这么期待的
作者丨Big Cloud编译 | 武明利,责编丨Carol出品 | AI科技大本营(ID:rgznai100)本报告将深入探讨亚太地区各个背景、不同年龄和不同地点的专业人员对2019/2020年的见解。今年贡献最大的地区来自新加坡和澳大利亚。因为这些是我们最大的数据点&…

AWS攻略——使用CodeBuild进行自动化构建和部署静态网页
首先声明下,使用“CodeBuild”部署并不是“正统”的方案,因为AWS提供了“CodeDeploy”。如果不希望引入太多基础设施,可以考虑直接使用CodeBuild进行部署。(转载请指明出于breaksoftware的csdn博客) 创建构建项目 kro…
我们需要什么样的数据架构?
作者 | Stephanie shen编译 | 火火酱,责编丨Carol出品 | AI科技大本营(ID:rgznai100)在大数据和数据科学的新时代,对企业而言,一定要有与业务流程保持一致的中心化数据架构,该架构能随业务增长而扩展&#…

Windows Server 2008 R2 之二十九故障转移群集(一)(
关于Windows Server 2008故障转移群集见http://technet.microsoft.com/zh-cn/library/cc732488(WS.10).aspx实验环境:两台已安装好Windows Server 2008 R2的计算机R2DC01、R2DC02,均为DC、DNS,域名为HBYCRSJ.COM,均有两块网卡。分别为心跳网络和本地连接…

基于多核DSP处理器DM8168的视频处理方法
摘要:随着1080P高清视频以及4K超高清晰视频的普及和应用,基于传统单核DSP处理器的视频信息处理已有些力不从心。为此TI公司推出了一款专门用于高清视频处理的多核DSP处理器,它拥有4个不同类型的处理器,使得视频处理达到了一个更高…

AWS攻略——使用CodeBuild进行自动化构建和部署Lambda(Python)
Aws Lambda是Amazon推出的“无服务架构”服务。我们只需要简单的上传代码,做些简单的配置,便可以使用。而且它是按运行时间收费,这对于低频访问的服务来说很划算。具体的介绍可以常见aws lambda的官网。(转载请指明出于breaksoftw…

vmware 添加 磁盘 空间
VMware安装linux的时候默认分配的空间是4GB,可能会不够,这个时候可以通过增加一块虚拟硬盘,将/usr或其他内容拷贝过去解决这个问题:创建虚拟硬盘1、关闭VM中正在运行的虚拟系统;2、在虚拟系统名称上点右键-…
Python爬取考研数据:所有985高校、六成211高校均可调剂
又到了一年一度的考研出分时间啦,近期有不少朋友让笔者帮他们分析如何提前做好调剂。复试与调剂总是密不可分。今天,给大家分享一些调剂的重要知识点,希望你在调剂的时候,能明白调剂的趋势与规则。也许,大家对于调剂的…

iOS审核秘籍】提审资源检查大法
iOS审核秘籍】提审资源检查大法 2015/11/27阅读(752)评论(1)收藏(6)加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品点此查看详情! 本篇主要是提审…

谈一次单元测试驱动代码重构
目前团队并没有QA岗,而且在很长一段时间内,可能也不会设立QA岗,所以我们需要RD保证代码的质量。而鉴于人类天生的“惰性”,很多时候质量完全依赖于作者的能力以及职业素质。于是我在团队内推动单元测试,并要求提升测试…

新机会在广州拓波
公司简介广州拓波软件科技有限公司的前身为 Turbomail工作室,由广州华工信息软件(集团)有限公司于2002 年成立,是一家专业研发电子邮件系统、企业即时通信和企业短信的开发组织,2005年TurboMail工作室正式发布1.0.2版本…
关于正则表达式,这篇都讲清楚了
作者 | 猪哥来源 | 裸睡的猪(ID:rgznai100)目前越来越多的网站、编辑器、编程语言都已支持一种叫“正则表达式”的字符串查找“公式”,有过编程经验的同学都应该了解正则表达式(Regular Expression 简写regex)是什么东…

MJExtension简介
MJExtension简介 前言:关于MJExtension更多的使用,可以到github网站上根据详述学习。 字典转模型比较流行的第三方框架 Mantle所有模型都必须继承自MTModel JSONModel所有模型都必须继承自JSONModel MJExtension不需要强制继承任何其他类 框架需要考虑的…