[Java] 用GUI展示class文件顶层的 access_flags

背景

The Java® Virtual Machine Specification 中的 4.1. The ClassFile Structure 这一小节有对 access_flags 的介绍 ⬇️

我们可以借助 trae 来生成一个 Python 脚本,这个 Python 提供图形化界面,让用户选择 class 文件,然后展示这个 class 文件顶层的 access_flags。示例效果如下图所示 ⬇️

要点

1. class 文件格式

class 文件的格式如下表所示 ⬇️ (原始内容来自 4.1. The ClassFile Structure)

类型 字段
u4 magic
u2 minor_version
u2 major_version
u2 constant_pool_count
cp_info constant_poolconstant_pool_count-1
u2 access_flags
u2 this_class
u2 super_class
u2 interfaces_count
u2 interfacesinterfaces_count
u2 fields_count
field_info fieldsfields_count
u2 methods_count
method_info methodsmethods_count
u2 attributes_count
attribute_info attributesattributes_count

本文只关心 access_flags,上表中将它用粗体展示了。

2. 如何借助 javap 命令查看 access_flags

如果我们想查看 Main.class 的 access_flags,可以执行下方的命令 ⬇️

bash 复制代码
javap -v Main.class

正文

生成代码的过程

4.1. The ClassFile Structure 中介绍了 class 文件的结构,要想获取 access_flags,我们需要跳过下图红色框所示的这些内容 ⬇️

其中

  • magic 长度固定为 4 字节
  • minor_version 长度固定为 2 字节
  • major_version 长度固定为 2 字节
  • constant_pool_count 长度固定为 2 字节
  • constant_pool 长度 不固定

所以难点在于如何跳过 constant_pool。根据 4.4. The Constant Pool 中的描述可知,constant_pool 共支持 17 种 tag ⬇️

trae 帮我汇总的常量池中各个 tag 的长度信息

(这一小节的内容由 trae 生成,我做了些调整)

来源: JVMS §4.4, Table 4.4-A (Java SE 27)

每个常量池条目以 1 字节的 tag 开头, tag 之后跟着若干字节的附加信息, 格式由 tag 决定。 下表按 tag 值升序排列, "字节数"指 tag 之后 的附加信息长度, 各结构的字段布局来自 §4.4.1--§4.4.12。

Tag 常量类型 结构 (tag 之后) 字节数 位置
1 CONSTANT_Utf8 u2 length; u1 bytes[length] 变长 (2 + length) §4.4.7
3 CONSTANT_Integer u4 bytes 4 §4.4.4
4 CONSTANT_Float u4 bytes 4 §4.4.4
5 CONSTANT_Long u4 high_bytes; u4 low_bytes 8 §4.4.5
6 CONSTANT_Double u4 high_bytes; u4 low_bytes 8 §4.4.5
7 CONSTANT_Class u2 name_index 2 §4.4.1
8 CONSTANT_String u2 string_index 2 §4.4.3
9 CONSTANT_Fieldref u2 class_index; u2 name_and_type_index 4 §4.4.2
10 CONSTANT_Methodref u2 class_index; u2 name_and_type_index 4 §4.4.2
11 CONSTANT_InterfaceMethodref u2 class_index; u2 name_and_type_index 4 §4.4.2
12 CONSTANT_NameAndType u2 name_index; u2 descriptor_index 4 §4.4.6
15 CONSTANT_MethodHandle u1 reference_kind; u2 reference_index 3 §4.4.8
16 CONSTANT_MethodType u2 descriptor_index 2 §4.4.9
17 CONSTANT_Dynamic u2 bootstrap_method_attr_index; u2 name_and_type_index 4 §4.4.10
18 CONSTANT_InvokeDynamic u2 bootstrap_method_attr_index; u2 name_and_type_index 4 §4.4.10
19 CONSTANT_Module u2 name_index 2 §4.4.11
20 CONSTANT_Package u2 name_index 2 §4.4.12
备注
  • u1 = 无符号 1 字节, u2 = 无符号 2 字节(大端序), u4 = 无符号 4 字节(大端序)。
  • CONSTANT_Utf8 是唯一变长的条目, 总长度 = 1(tag) + 2(length) + length。
  • CONSTANT_Long(5) 和 CONSTANT_Double(6) 虽然只占 8 字节, 但在常量池中占用 2 个 slot, 即常量池索引会跳过一个位置 --- 遍历常量池时必须特殊处理。
  • 同一小节内的常量类型结构完全相同, 仅 tag 值不同:
    • §4.4.2: Fieldref / Methodref / InterfaceMethodref
    • §4.4.4: Integer / Float
    • §4.4.5: Long / Double
    • §4.4.10: Dynamic / InvokeDynamic

完整的代码

经过与 trae 和 豆包 的若干轮互动,最终得到了如下的 Python 代码

python 复制代码
import struct
import tkinter as tk
from tkinter import filedialog, messagebox, ttk

# class 文件魔数: 0xCAFEBABE
MAGIC_NUMBER = 0xCAFEBABE

# 常量池 tag -> (类型名称, 后续字节数)
# 注: CONSTANT_Long(5) 和 CONSTANT_Double(6) 占用 2 个 slot
CONSTANT_POOL_TAGS = {
    1: ("CONSTANT_Utf8", None),            # 变长: 2字节 length + length 字节
    3: ("CONSTANT_Integer", 4),
    4: ("CONSTANT_Float", 4),
    5: ("CONSTANT_Long", 8),              # 占用 2 个 slot
    6: ("CONSTANT_Double", 8),            # 占用 2 个 slot
    7: ("CONSTANT_Class", 2),
    8: ("CONSTANT_String", 2),
    9: ("CONSTANT_Fieldref", 4),
    10: ("CONSTANT_Methodref", 4),
    11: ("CONSTANT_InterfaceMethodref", 4),
    12: ("CONSTANT_NameAndType", 4),
    15: ("CONSTANT_MethodHandle", 3),
    16: ("CONSTANT_MethodType", 2),
    17: ("CONSTANT_Dynamic", 4),
    18: ("CONSTANT_InvokeDynamic", 4),
    19: ("CONSTANT_Module", 2),
    20: ("CONSTANT_Package", 2),
}

# access_flags 位掩码 (JVMS §4.1 Table 4.1-B)
ACCESS_FLAGS = [
    ("ACC_PUBLIC", 0x0001,
     "Declared public; may be accessed from outside its package."),
    ("ACC_FINAL", 0x0010, "Declared final; no subclasses allowed."),
    ("ACC_SUPER", 0x0020,
     "Treat superclass methods specially when invoked by the "
     "invokespecial instruction."),
    ("ACC_INTERFACE", 0x0200, "Is an interface, not a class."),
    ("ACC_ABSTRACT", 0x0400, "Declared abstract; must not be instantiated."),
    ("ACC_SYNTHETIC", 0x1000,
     "Declared synthetic; not present in the source code."),
    ("ACC_ANNOTATION", 0x2000, "Declared as an annotation interface."),
    ("ACC_ENUM", 0x4000, "Declared as an enum class."),
    ("ACC_MODULE", 0x8000, "Is a module, not a class or interface."),
]


def skip_constant_pool(data, offset, count):
    """跳过常量池, 返回 access_flags 的偏移量。
    常量池索引从 1 到 count-1。
    """
    index = 1
    while index < count:
        if offset >= len(data):
            raise ValueError("常量池数据不完整, 文件可能被截断")
        tag = data[offset]
        offset += 1
        if tag not in CONSTANT_POOL_TAGS:
            raise ValueError(f"未知的常量池 tag: {tag} (索引 {index})")
        name, size = CONSTANT_POOL_TAGS[tag]
        if name == "CONSTANT_Utf8":
            if offset + 2 > len(data):
                raise ValueError(
                    "CONSTANT_Utf8 长度字段不完整, 文件可能被截断")
            (length,) = struct.unpack_from(">H", data, offset)
            offset += 2
            if offset + length > len(data):
                raise ValueError(
                    "CONSTANT_Utf8 内容超出文件末尾, 文件可能被截断")
            offset += length
        else:
            if offset + size > len(data):
                raise ValueError(f"{name} 数据不完整, 文件可能被截断")
            offset += size
        # CONSTANT_Long 和 CONSTANT_Double 占 2 个 slot
        if name in ("CONSTANT_Long", "CONSTANT_Double"):
            index += 2
        else:
            index += 1
    return offset


def parse_access_flags(file_path):
    """解析 class 文件, 返回 (minor, major, constant_pool_count, access_flags)。"""
    with open(file_path, "rb") as f:
        data = f.read()

    if len(data) < 10:
        raise ValueError("文件太小, 不是有效的 class 文件")

    # 格式字符串 ">IHHH" 含义:
    #   >  --- 大端序(网络字节序), class 文件采用大端序
    #   I  --- 无符号 4 字节整数, 读取 magic (魔数)
    #   H  --- 无符号 2 字节整数, 读取 minor_version (次版本号)
    #   H  --- 无符号 2 字节整数, 读取 major_version (主版本号)
    #   H  --- 无符号 2 字节整数, 读取 constant_pool_count (常量池条目数)
    magic, minor, major, cp_count = struct.unpack_from(">IHHH", data, 0)
    if magic != MAGIC_NUMBER:
        raise ValueError(f"魔数不匹配, 期望 0xCAFEBABE, 实际为 0x{magic:08X}")

    # constant_pool_count 等于常量池条目数 + 1, 因此最小为 1
    if cp_count < 1:
        raise ValueError(f"constant_pool_count 非法: {cp_count} (必须 >= 1)")

    offset = 10  # magic(4) + minor(2) + major(2) + constant_pool_count(2)
    offset = skip_constant_pool(data, offset, cp_count)

    if offset + 2 > len(data):
        raise ValueError("access_flags 字段不完整, 文件可能被截断")
    (access_flags,) = struct.unpack_from(">H", data, offset)
    return minor, major, cp_count, access_flags


class AccessFlagsViewer:
    def __init__(self, root):
        self.root = root
        self.root.title("Class 文件 access_flags 查看器")
        self.root.resizable(False, False)

        self.file_path = tk.StringVar(value="尚未选择文件")
        self.minor_version = tk.StringVar(value="-")
        self.major_version = tk.StringVar(value="-")
        self.cp_count = tk.StringVar(value="-")
        self.access_flags_hex = tk.StringVar(value="-")

        self._build_ui()

        # 宽度固定 880, 高度取内容自然高度(明细表恰好 9 行)
        self.root.update_idletasks()
        self.root.geometry(f"880x{self.root.winfo_reqheight()}")

    def _build_ui(self):
        pad = {"padx": 12, "pady": 8}

        # 文件选择行
        file_frame = ttk.Frame(self.root)
        file_frame.pack(fill="x", **pad)

        ttk.Button(file_frame, text="选择 class 文件",
                   command=self._choose_file).pack(side="left")
        path_label = ttk.Label(file_frame, textvariable=self.file_path,
                               foreground="gray", wraplength=600)
        path_label.pack(side="left", padx=(8, 0))

        # 基础信息区
        info_frame = ttk.LabelFrame(self.root, text="基础信息")
        info_frame.pack(fill="x", **pad)

        rows = [
            ("次版本号 (minor_version):", self.minor_version),
            ("主版本号 (major_version):", self.major_version),
            ("常量池条目数 (constant_pool_count):", self.cp_count),
            ("access_flags (十六进制):", self.access_flags_hex),
        ]
        for i, (label_text, value_var) in enumerate(rows):
            ttk.Label(info_frame, text=label_text).grid(
                row=i, column=0, sticky="w", padx=8, pady=4)
            value_label = ttk.Label(info_frame, textvariable=value_var,
                                    font=("Menlo", 11, "bold"))
            value_label.grid(row=i, column=1, sticky="w", padx=8, pady=4)

        info_frame.columnconfigure(1, weight=1)

        # flags 明细表
        flags_frame = ttk.LabelFrame(self.root, text="access_flags 位明细")
        flags_frame.pack(fill="x", **pad)

        columns = ("flag", "value", "set", "interpretation")
        self.tree = ttk.Treeview(flags_frame, columns=columns,
                                 show="headings", height=9)
        self.tree.heading("flag", text="Flag Name")
        self.tree.heading("value", text="Value")
        self.tree.heading("set", text="Set?")
        self.tree.heading("interpretation", text="Interpretation")
        self.tree.column("flag", width=140)
        self.tree.column("value", width=80)
        self.tree.column("set", width=60)
        self.tree.column("interpretation", width=560)
        # 配置高亮 tag: 用于 "Set?" 为 Yes 的行
        self.tree.tag_configure("set", background="#FFF3B0",
                                foreground="#B8860B")
        self.tree.pack(fill="x")

    def _choose_file(self):
        path = filedialog.askopenfilename(
            title="选择 class 文件",
            filetypes=[("Java class 文件", "*.class"), ("所有文件", "*.*")],
        )
        if not path:
            return

        self.file_path.set(path)
        try:
            minor, major, cp_count, flags = parse_access_flags(path)
        except (ValueError, OSError) as e:
            messagebox.showerror("错误", str(e))
            self._reset_results()
            return

        self.minor_version.set(str(minor))
        self.major_version.set(str(major))
        self.cp_count.set(str(cp_count))
        self.access_flags_hex.set(f"0x{flags:04X}")
        self._update_flags_table(flags)

    def _update_flags_table(self, flags):
        self.tree.delete(*self.tree.get_children())
        for name, value, desc in ACCESS_FLAGS:
            is_set = "Yes" if flags & value else "No"
            tags = ("set",) if flags & value else ()
            self.tree.insert("", "end",
                             values=(name, f"0x{value:04X}", is_set, desc),
                             tags=tags)

    def _reset_results(self):
        for var in (self.minor_version, self.major_version, self.cp_count,
                    self.access_flags_hex):
            var.set("-")
        self.tree.delete(*self.tree.get_children())


def main():
    root = tk.Tk()
    AccessFlagsViewer(root)
    root.mainloop()


if __name__ == "__main__":
    main()

运行结果

请将以上代码保存为 access_flags_viewer.py,使用如下命令可以运行它

bash 复制代码
python3 access_flags_viewer.py

初始的效果如下图所示 ⬇️ (此时尚未选择 class 文件)

选择一个 class 文件后,就会展示这个 class 文件顶层的 access_flags。示例效果如下 ⬇️

补充说明

借助 javap 命令来查看 access_flags

可以借助 javap 命令来查看 class 文件的 access_flags。示例如下(假设我们已经有一个名为 Main.class 的文件了)

bash 复制代码
javap -v Main.class

示例运行结果如下图所示 ⬇️

运行环境提示

  1. 运行脚本需要本机有 Python
  2. 如果您没有安装 Tkinter 的话,也需要安装一下才能运行本文提供的 Python 脚本

参考资料

相关推荐
kimnoic1 小时前
Python操作xlwings的实例详解
开发语言·python
2601_957883841 小时前
2026年10月 外星人笔记本维修须知
python·电脑
泡茶喝茶写代码1 小时前
A股量化数据工程:从 REST 接口到策略信号(第 17 篇):创新高与新低扫描
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
IT_陈寒1 小时前
Vue的数组更新把我坑惨了
前端·人工智能·后端
VIP_CQCRE2 小时前
用 Ace Data Cloud 接入 OpenAI 兼容语音转文字:一份能直接运行的 API 指南
python·openai·api·语音识别·acedatacloud
高晶2 小时前
Android 智能语音开发新手入门实战
后端·程序员
励志不掉头发的内向程序员2 小时前
鼠标点一下,图形是怎么画出来的?拆解 CAD 的 Action 状态机
后端·架构
高晶2 小时前
Android 机器视觉新手入门:从扫码到人脸实战
后端·程序员