手写JVM(二十)-方法调用和返回指令

代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。

1.返回指令

方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。

  • 包括ireturn (当返回值是boolean、 byte、 char、 short和int类型时使用) 、lreturn、 freturn、dreturn和areturn
  • 另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

举例:

通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。

如果当前返回的是synchronized方法,那么还会执行一个隐含的monitorexit指令,退出临界区。

最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。

在/instructions/control目录下创建return.go,在其中定义返回指令,代码如下:

package control

import (
    "jvmgo/ch07/instructions/base"
    "jvmgo/ch07/rtda"
)

// Return void from method
type RETURN struct{ base.NoOperandsInstruction }

func (self *RETURN) Execute(frame *rtda.Frame) {
    // 无返回值,直接将当前帧从Java虚拟机栈中弹出
    frame.Thread().PopFrame()
}

// Return reference from method
type ARETURN struct{ base.NoOperandsInstruction }

func (self *ARETURN) Execute(frame *rtda.Frame) {
    // 获取当前线程
    thread := frame.Thread()
    // 弹出当前帧
    currentFrame := thread.PopFrame()
    // 获取前一帧,也就是调用方栈帧
    invokerFrame := thread.TopFrame()
    // 弹出当前帧的操作数栈顶引用变量的值
    ref := currentFrame.OperandStack().PopRef()
    // 将返回值推入前一帧的操作数栈顶
    invokerFrame.OperandStack().PushRef(ref)
}

// Return double from method
type DRETURN struct{ base.NoOperandsInstruction }

func (self *DRETURN) Execute(frame *rtda.Frame) {
    thread := frame.Thread()
    currentFrame := thread.PopFrame()
    invokerFrame := thread.TopFrame()
    val := currentFrame.OperandStack().PopDouble()
    invokerFrame.OperandStack().PushDouble(val)
}

// Return float from method
type FRETURN struct{ base.NoOperandsInstruction }

func (self *FRETURN) Execute(frame *rtda.Frame) {
    thread := frame.Thread()
    currentFrame := thread.PopFrame()
    invokerFrame := thread.TopFrame()
    val := currentFrame.OperandStack().PopFloat()
    invokerFrame.OperandStack().PushFloat(val)
}

// Return int from method
type IRETURN struct{ base.NoOperandsInstruction }

func (self *IRETURN) Execute(frame *rtda.Frame) {
    thread := frame.Thread()
    currentFrame := thread.PopFrame()
    invokerFrame := thread.TopFrame()
    val := currentFrame.OperandStack().PopInt()
    invokerFrame.OperandStack().PushInt(val)
}

// Return double from method
type LRETURN struct{ base.NoOperandsInstruction }

func (self *LRETURN) Execute(frame *rtda.Frame) {
    thread := frame.Thread()
    currentFrame := thread.PopFrame()
    invokerFrame := thread.TopFrame()
    val := currentFrame.OperandStack().PopLong()
    invokerFrame.OperandStack().PushLong(val)
}

 

6条返回指令都不需要操作数。代码简单,不在阐述。

在/rtda/thread.go加上TopFrame()方法,其实和CurrentFrame()代码一样,为了避免混淆,使用不同的名称。

func (self *Thread) TopFrame() *Frame {
    return self.stack.top()
}

 

2.方法调用指令

方法调用指令

1.方法调用指令: invokevirtual、 invokeinterface、 invokespecial、invokestatic、invokedynamic

以下5条指令用于方法调用:

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派) ,支持多态。这也是Java语言中最常见的方法分派方式。
  • invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
  • invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法(super调用)。(注意,这三种方法不存在方法重写)这些方法都是静态类型绑定的,不会在调用时进行动态派发。
  • invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的。
  • invokedynamic:调用动态绑定的方法,这个是JDK 1.7后新加入的指令,用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

我们只实现前4条

我们不考虑接口的静态方法和默认方法,所以要实现的这4条指令并没有完全满足Java虚拟机规范第8版的规定。

invokestatic指令

invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的。

完整代码

在\instructions\references目录下创建invokestatic.go文件,在其中定义invokestatic指令,代码如下:

package references

import (
    "jvmgo/ch07/instructions/base"
    "jvmgo/ch07/rtda"
    "jvmgo/ch07/rtda/heap"
)

// Invoke a class (static) method
type INVOKE_STATIC struct{ base.Index16Instruction }

func (self *INVOKE_STATIC) Execute(frame *rtda.Frame) {
    // 获取运行时常量池
    cp := frame.Method().Class().ConstantPool()
    // 通过索引获取方法符号引用
    methodRef := cp.GetConstant(self.Index).(*heap.MethodRef)
    // 解析方法
    resolvedMethod := methodRef.ResolvedMethod()
    // 判断方法是否是static
    if !resolvedMethod.IsStatic() {
        panic("java.lang.IncompatibleClassChangeError")
    }

    base.InvokeMethod(frame, resolvedMethod)
}

 

代码解释

结构体定义比较简单,直接看Execute()方法,假定解析符号引用后得到方法M。M必须是静态方法,否则抛出IncompatibleClassChangeError异常。M不能是类初始化方法。类初始化方法只能由Java虚拟机调用,不能使用invokestatic指令调用。但是这一规则由class文件验证器保证,所以这里不做检查。如果声明M的类还没有被初始化,则要先初始化该类。在7.8小节讨论类的初始化。对于invokestatic指令,M就是最终要执行的方法,调用InvokeMethod()函数执行该方法。

 

invokespecial指令

invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。(注意,这三种方法不存在方法重写)这些方法都是静态类型绑定的,不会在调用时进行动态派发。

完整代码

1.在/rtdata/operand_stack.go文件中添加GetRefFromTop()方法,返回距离操作数栈顶n个slot的引用变量。在传递参数之前,不能破坏操作数栈的状态,所以该方法的实现只是获取了引用变量,而没有从栈中弹出。比如GetRefFromTop(0)返回操作数栈顶引用,GetRefFromTop(1)返回从栈顶开始的倒数第二个引用。

func (self *OperandStack) GetRefFromTop(n uint) *heap.Object {
    return self.slots[self.size-1-n].ref
}

2.Go规定任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。所以我们在/rtdata/heap/class.go,将getPackageName()方法改为GetPackageName(),并记得添加Getter方法:

// getters
func (self *Class) Name() string {
    return self.name
}
func (self *Class) ConstantPool() *ConstantPool {
return self.constantPool
}
func (self *Class) Fields() []*Field {
return self.fields
}
func (self *Class) Methods() []*Method {
return self.methods
}
func (self *Class) SuperClass() *Class {
return self.superClass
}
func (self *Class) StaticVars() Slots {
return self.staticVars
}

 

3.在/rtdata/heap/class_hierarchy.go文件,将isSubClassOf()方法改为IsSubClassOf(),并添加IsSuperClassOf方法:

// c extends self
func (self *Class) IsSuperClassOf(other *Class) bool {
    return other.isSubClassOf(self)
}

 

上面这些代码只是为了下面更好操作,且代码简单,所以不在阐述

 

4.在instructions\references目录下创建invokespecial.go文件,invokespecial指令在第6章就定义好了,需要修改Execute()方法。

package references

import (
    "jvmgo/ch07/instructions/base"
    "jvmgo/ch07/rtda"
    "jvmgo/ch07/rtda/heap"
)

// Invoke instance method;
// special handling for superclass, private, and instance initialization method invocations
type INVOKE_SPECIAL struct{ base.Index16Instruction }

func (self *INVOKE_SPECIAL) Execute(frame *rtda.Frame) {
    // 获取当前class
    currentClass := frame.Method().Class()
    // 获取当前class的运行时常量池
    cp := currentClass.ConstantPool()
    // 通过索引获取方法符号引用
    methodRef := cp.GetConstant(self.Index).(*heap.MethodRef)
    // 解析需要调用的方法所属的类
    resolvedClass := methodRef.ResolvedClass()
    // 解析需要调用的方法
    resolvedMethod := methodRef.ResolvedMethod()
    // 如果方法是构造函数(init(...)方法),则声明该方法的类必须是之前通过方法符号引用解析出来的类
    if resolvedMethod.Name() == "<init>" && resolvedMethod.Class() != resolvedClass {
        panic("java.lang.NoSuchMethodError")
    }
    // 判断方法是否是static
    if resolvedMethod.IsStatic() {
        panic("java.lang.IncompatibleClassChangeError")
    }

    //从操作数栈中弹出this引用,GetRefFromTop()方法,该方法返回距离操作数栈顶n个单元格的引用变量
    ref := frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1)
    // 从操作数栈中弹出this引用,如果该引用是null,从操作数栈中弹出this引用,如果该引用是null
    if ref == nil {
        panic("java.lang.NullPointerException")
    }

    //确保protected方法只能被声明该方法的类或子类调用。如果违反这一规定,则抛出IllegalAccessError异常。
    if resolvedMethod.IsProtected() &&
        resolvedMethod.Class().IsSuperClassOf(currentClass) &&
        resolvedMethod.Class().GetPackageName() != currentClass.GetPackageName() &&
        ref.Class() != currentClass &&
        !ref.Class().IsSubClassOf(currentClass) {

        panic("java.lang.IllegalAccessError")
    }

    //是因为调用超类的(非构造函数)方法需要特别处理
    //备用
    methodToBeInvoked := resolvedMethod
    //当前类的ACC_SUPER标志被设置
    if currentClass.IsSuper() &&
        //且调用的是超类中的函数
        resolvedClass.IsSuperClassOf(currentClass) &&
        //且不是构造函数
        resolvedMethod.Name() != "<init>" {
        //查找最终要调用的方法,使用了invokespecial ,他就会搜索类层次,找到最近的一个父类进行方法调用,得到正确的结果。现在编译器都会生成ACC_SUPER以支持一个正确的父类调用。
        methodToBeInvoked = heap.LookupMethodInClass(currentClass.SuperClass(),
            methodRef.Name(), methodRef.Descriptor())
    }

    if methodToBeInvoked == nil || methodToBeInvoked.IsAbstract() {
        panic("java.lang.AbstractMethodError")
    }

    base.InvokeMethod(frame, methodToBeInvoked)
}

代码解释

先拿到当前类、当前常量池、方法符号引用,然后解析符号引用,拿到解析后的类和方法。

 

假定从方法符号引用中解析出来的类是C,方法是M。如果M是构造函数,则声明M的类必须是C,否则抛出NoSuchMethodError异常。如果M是静态方法,则抛出IncompatibleClassChangeError异常。

 

从操作数栈中弹出this引用,如果该引用是null,抛出NullPointerException异常。注意,在传递参数之前,不能破坏操作数栈的状态。GetRefFromTop()方法前面已给出。

 

确保protected方法只能被声明该方法的类或子类调用。如果违反这一规定,则抛出IllegalAccessError异常。

 

如果调用的中超类中的函数,但不是构造函数,且当前类的ACC_SUPER标志被设置,需要一个额外的过程查找最终要调用的方法;否则前面从方法符号引用中解析出来的方法就是要调用的方法。

 

如果查找过程失败,或者找到的方法是抽象的,抛出AbstractMethodError异常。最后,如果一切正常,就调用方法。这里之所以这么复杂,是因为调用超类的(非构造函数)方法需要特别处理。

至于为什么可以参考官网,What is the purpose of the ACC_SUPER access flag on Java Class files? – Stack Overflow

如果使用了invokespecial ,他就会搜索类层次,找到最近的一个父类进行方法调用,得到正确的结果。现在编译器都会生成ACC_SUPER以支持一个正确的父类调用。

 

invokevirtual指令

invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派) ,支持多态。这也是Java语言中最常见的方法分派方式。

整体代码

invokevirtual指令也已经在之前定义好了,修改Execute()方法:

package references

import (
    "fmt"
    "jvmgo/ch07/instructions/base"
    "jvmgo/ch07/rtda"
    "jvmgo/ch07/rtda/heap"
)

// Invoke instance method; dispatch based on class
type INVOKE_VIRTUAL struct{ base.Index16Instruction }

func (self *INVOKE_VIRTUAL) Execute(frame *rtda.Frame) {
    // 获取当前class
    currentClass := frame.Method().Class()
    // 获取当前class的运行时常量池
    cp := currentClass.ConstantPool()
    // 通过索引获取方法符号引用
    methodRef := cp.GetConstant(self.Index).(*heap.MethodRef)
    // 解析需要调用的方法
    resolvedMethod := methodRef.ResolvedMethod()
    // 判断方法是否是static
    if resolvedMethod.IsStatic() {
        panic("java.lang.IncompatibleClassChangeError")
    }
    // 获取距离操作数栈顶n个slot的引用变量,也就是this引用,GetRefFromTop()方法,该方法返回距离操作数栈顶n个单元格的引用变量
    ref := frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1)
    // 获取this引用,如果该引用是null
    if ref == nil {
        // hack!
        //暂时要用hack的方式调用System.out.println()方法,
        if methodRef.Name() == "println" {
            _println(frame.OperandStack(), methodRef.Descriptor())
            return
        }

        panic("java.lang.NullPointerException")
    }

    //确保protected方法只能被声明该方法的类或子类调用。如果违反这一规定,则抛出IllegalAccessError异常。
    if resolvedMethod.IsProtected() &&
        resolvedMethod.Class().IsSuperClassOf(currentClass) &&
        resolvedMethod.Class().GetPackageName() != currentClass.GetPackageName() &&
        ref.Class() != currentClass &&
        !ref.Class().IsSubClassOf(currentClass) {

        panic("java.lang.IllegalAccessError")
    }

    //从对象的类中查找真正要调用的方法。
    methodToBeInvoked := heap.LookupMethodInClass(ref.Class(),
    methodRef.Name(), methodRef.Descriptor())
    //如果找不到方法,或者找到的是抽象方法,则需要抛出AbstractMethodError异常
    if methodToBeInvoked == nil || methodToBeInvoked.IsAbstract() {
        panic("java.lang.AbstractMethodError")
    }

    base.InvokeMethod(frame, methodToBeInvoked)
}

// hack!
func _println(stack *rtda.OperandStack, descriptor string) {
    switch descriptor {
    case "(Z)V":
        fmt.Printf("%v\n", stack.PopInt() != 0)
    case "(C)V":
        fmt.Printf("%c\n", stack.PopInt())
    case "(I)V", "(B)V", "(S)V":
        fmt.Printf("%v\n", stack.PopInt())
    case "(F)V":
        fmt.Printf("%v\n", stack.PopFloat())
    case "(J)V":
        fmt.Printf("%v\n", stack.PopLong())
    case "(D)V":
        fmt.Printf("%v\n", stack.PopDouble())
    default:
    panic("println: " + descriptor)
    }
    stack.PopRef()
}

代码解释

代码和invokespecial指令差不多的,不在说,如果this引用为空,而是System.out.println()方法,

则要用hack的方式调用System.out.println方法,

_println()函数如下:

从对象的类中查找真正要调用的方法。如果找不到方法,或者找到的是抽象方法,则需要抛出AbstractMethodError异常,否则一切正常,调用方法。

 

invokeinterface指令

invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。

完整代码

Go规定任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。所以,在/rtdata/heap/class_hierarchy.go文件中,将isImplements()方法改名为IsImplements()方法。

在\instructions\references目录下创建invokeinterface.go文件,在其中定义invokeinterface指令,代码如下:

package references

import (
    "jvmgo/ch08/instructions/base"
    "jvmgo/ch08/rtda"
    "jvmgo/ch08/rtda/heap"
)

// Invoke interface method
type INVOKE_INTERFACE struct {
    index uint
    // count uint8
    // zero uint8
}

func (self *INVOKE_INTERFACE) FetchOperands(reader *base.BytecodeReader) {
    self.index = uint(reader.ReadUint16())
    // 下面两个字节读出来丢弃
    reader.ReadUint8() // count
    reader.ReadUint8() // must be 0
}

func (self *INVOKE_INTERFACE) Execute(frame *rtda.Frame) {
    // 获取当前class的运行时常量池
    cp := frame.Method().Class().ConstantPool()
    //拿到并解析接口方法符号引用
    methodRef := cp.GetConstant(self.index).(*heap.InterfaceMethodRef)
    resolvedMethod := methodRef.ResolvedInterfaceMethod()
    //解析后的方法是静态方法或私有方法,则抛出IncompatibleClassChangeError异常
    if resolvedMethod.IsStatic() || resolvedMethod.IsPrivate() {
        panic("java.lang.IncompatibleClassChangeError")
    }
    //从操作数栈中弹出this引用,如果引用是null,则抛出NullPointerException异常。
    ref := frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1)
    if ref == nil {
        panic("java.lang.NullPointerException") // todo
    }
    //引用所指对象的类没有实现解析出来的接口,则抛出IncompatibleClassChangeError异常。
    if !ref.Class().IsImplements(methodRef.ResolvedClass()) {
        panic("java.lang.IncompatibleClassChangeError")
    }

    //查找最终要调用的方法。
    methodToBeInvoked := heap.LookupMethodInClass(ref.Class(),
    methodRef.Name(), methodRef.Descriptor())
    //如果找不到,或者找到的方法是抽象的,则抛出AbstractMethodError异常。
    if methodToBeInvoked == nil || methodToBeInvoked.IsAbstract() {
        panic("java.lang.AbstractMethodError")
    }
    //如果找到的方法不是public,则抛出IllegalAccessError异常,
    if !methodToBeInvoked.IsPublic() {
        panic("java.lang.IllegalAccessError")
    }
    //否则,一切正常,调用方法:
    base.InvokeMethod(frame, methodToBeInvoked)
}

 

代码解释

和其他三条方法调用指令不同,在字节码中,invokeinterface指令的操作码后面跟着4字节而非2字节。前两字节的含义和其他指令相同,是个uint16运行时常量池索引。第3字节的值是给方法传递参数需要的slot数,其含义和给Method结构体定义的argSlotCount字段相同。正如我们所知,这个数是可以根据方法描述符计算出来的,它的存在仅仅是因为历史原因。第4字节是留给Oracle的某些Java虚拟机实现用的,它的值必须是0。该字节的存在是为了保证Java虚拟机可以向后兼容。

invokeinterface指令的FetchOperands()方法,所以将第三,第四两个字节读出来丢弃

 

下面看Execute()方法,第一部分代码如下:先从运行时常量池中拿到并解析接口方法符号引用,如果解析后的方法是静态方法或私有方法,则抛出IncompatibleClassChangeError异常。

 

从操作数栈中弹出this引用,如果引用是null,则抛出NullPointerException异常。如果引用所指对象的类没有实现解析出来的接口,则抛出IncompatibleClassChangeError异常。

 

查找最终要调用的方法。如果找不到,或者找到的方法是抽象的,则抛出AbstractMethodError异常。如果找到的方法不是public,则抛出IllegalAccessError异常,否则,一切正常,调用方法

 

总结:
invokestatic指令调用静态方法。invokespecial指令调用私有方法和构造函数,因为私有方法和构造函数不需要动态绑定,所以invokespecial指令可以加快方法调用速度。其次,使用super关键字调用超类中的方法不能使用invokevirtual指令,否则会陷入无限循环。

invokeinterface指令和invokevirtual指令的区别在于:当Java虚拟机通过invokevirtual调用方法时,this引用指向某个类(或其子类)的实例。因为类的继承层次是固定的,所以虚拟机可以使用一种叫作vtable(Virtual Method Table)的技术加速方法查找。但是当通过invokeinterface指令调用接口方法时,因为this引用可以指向任何实现了该接口的类的实例,所以无法使用vtable技术。

 

既然提到了,就扩展一下:

方法的调用:虚方法表(Virtual Method Table)

  • 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table) (非虚方法不会出现在表中,因为已经确定)来实现。使用索引表来代替查找。
  • 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
  • 那么虚方法表什么时候被创建呢? 虚方法表会在类加载的链接阶段(中的解析阶段,将常量池内的符号引用转换为直接引用的过程)被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
  • 子类从基类中拷贝一份虚函数表,使子类的虚函数表指针指向新的虚函数表,如果子类中覆写了基类的虚函数,则将函数表中覆写函数的函数指针替换为子类覆写的函数指针,如果子类中有新增的虚函数,则在该子类的虚函数表中追加新增的虚函数指针。

1.举例1:

白色为类实现或重写,则直接指向实现或重写的类

 

举例2:

package java3;


interface Friendly {
    void sayHello();
    void sayGoodbye();
}
class Dog {
    public void sayHello() {
    }
    public String toString() {
        return "Dog";
    }
}
class Cat implements Friendly {
    public void eat() {
    }
    public void sayHello() {
    }
    public void sayGoodbye() {
    }
    protected void finalize() {
    }
    public String toString(){
        return "Cat";
    }
}

class CockerSpaniel extends Dog implements Friendly {
    public void sayHello() {
        super.sayHello();
    }
    public void sayGoodbye() {
    }
}

public class VirtualMethodTable {
}

 

白色为子类实现或重写,可以对照代码来看一下

Dog虚方法表

CockerSpaniel虚方法表:

cat虚方法表:

建立虚方法表:在调用父类有子类没有的方法时,直接到虚方法表中去寻找即可,提高了效率。

4条方法调用指令和6条返回指令都准备好了,还需要修改instructions\factory.go文件,在其中增加这些指令的case语句。把factory.go文件中本章节相关指令项的注释去掉就行。

 

3.参考

尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ

周志明:深入理解java虚拟机

张秀宏:自己动手写Java虚拟机 (Java核心技术系列)

GO语言官网:Standard library – Go Packages

Java虚拟机规范:Chapter 4. The class File Format (oracle.com)

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇