【Python】读取elf文件,更新a2l文件中变量地址

前言

a2l文件是汽车行业常见的用于标定的数据库文件。在开发过程中,通常由MATLAB或者Excel生成a2l文件,但变量的地址是需要load elf文件去更新的,一般使用的是Vector的CANape工具。
下面记录和分享一下通过脚本去更新a2l文件。

1 使用CANoe中的ASAP2Updater

1.1 ASAP2Updater工具路径

一般在CANoe安装路径下,比如:

C:\Program Files\Vector CANoe 12.0\ASAP2Updater\Exec\ASAP2Updater.exe

同级目录下存在手册Manual.pdf。

1.2 使用方式

Manual.pdf
参考“Manual.pdf”手册中的3.2章节(如上图所示),则可在命令行中执行:

C:\Program Files\Vector CANoe 12.0\ASAP2Updater\Exec\ASAP2Updater.exe -I Test.a2l -A Test.elf -O Gen.a2l

需要注意的是,读取elf文件时,需要指定elf的格式,目前常用的是ELF 32 Bit。这个和在Vector工具ASAP2 Studio中选择elf格式一样的,如下图。
请添加图片描述
参考手册中的3.4章节,需要创建一个名称为“UPDATER.INI”的文件,在其中写入如下信息,并放到执行命令行的路径中。

[OPTIONS]
MAP_FORMAT = 31

如果有其他options需要设定,请参考手册。

2 使用Python

2.1 处理a2l文件

2.1.1 三方库

使用pya2l,参考: https://pypi.org/project/pya2l/

2.1.2 读取a2l文件

pya2l读取a2l文件后的格式是tree,这个格式不方便读取和编辑,可以转换成json,再到dict。参考以下步骤可读入a2l并转换成dict。
1) 导入json和pya2l中的A2lParser

import json
from pya2l.parser import A2lParser as Parser

2)读入a2l

self.parser = Parser()
# 读取a2l文件,二进制。
with open(self.a2l_file, "rb") as f:
    byte = f.read()
    f.close()
# a2l转换成内部的tree。
r_ast = self.parser.tree_from_a2l(byte)
# tree转换成json,方便修改
r_json_bytes = self.parser.json_from_tree(r_ast)
# load json,转换成字典,方便修改。
self.r_json_py = json.loads(r_json_bytes.decode())
  • tree_from_a2l()读取的是byte格式数据,不是字符串。
  • json_from_tree()可将tree转换成json格式。
  • json.loads()将json格式转为字典,便于编辑。
  • 2.1.3 保存a2l文件

    保存成a2l文件的方式和读取a2l的步骤刚好相反,可参考如下:

    # json转换成tree。
    w_json = json.dumps(self.r_json_py, indent=4)
    w_ast = self.parser.tree_from_json(w_json.encode())
    # tree转换成a2l。
    w_a2l = self.parser.a2l_from_tree(w_ast, indent=4).decode()  # SYMBOL_LINK没了
    w_a2l = self.add_SYMBOL_LINK(w_a2l)
    print(w_a2l, file=open(a2l_file, "w"))
    
  • json.dumps()将字典转换为json。
  • tree_from_json()将json转成tree,用于保存成a2l。
  • a2l_from_tree()将tree转换成byte格式,后面加个decode()即可转为str。
  • 注意 a2l_from_tree()返回的文本中丢失了SYMBOL_LINK,Vector工具更新地址会用到。可以用正则表达式添加,参考如下:

    regex = re.compile(r"(/begin (MEASUREMENT|CHARACTERISTIC) ([^ ]+) .*)")
    str_out = regex.sub(r"\1" + " SYMBOL_LINK " + r'"\3" 0', str_in)
    

    2.1.4 读取a2l文件中的RECORD_LAYOUT

    RECORD_LAYOUT是a2l中的数据类型。读取数据类型,便于对比a2l和elf变量类型是否相同。
    a2l中的基础类型一般是UBYTE、UWORD这种,在此基础上可以定义成其他的类型,比如Scalar_UBYTE。其本质还是基础类型,所以可以用一个字典表示这些类型,方便后面查找和对比。{basetype: set(data types)}
    打印的示例如下:

    record_dict: {'FLOAT32_IEEE': {'Lookup1D_FLOAT32_IEEE', 'FLOAT32_IEEE', 'Scalar_FLOAT32_IEEE', 'Axis_FLOAT32_IEEE'}, 'UBYTE': {'UBYTE', 'Scalar_UBYTE'}, 'ULONG': {'Scalar_ULONG', 'ULONG'}, 'UWORD': {'Scalar_UWORD', 'UWORD'}}
    

    读取RECORD_LAYOUT,保存成上述数据格式的示例代码如下:

    def read_record(self):
        """
        a2l中的RECORD_LAYOUT,数据类型。
        """
        project = self.r_json_py.get("PROJECT")
        modules = project.get("MODULE")
        for module in modules:  # MODULE长度是1
            records = module.get("RECORD_LAYOUT")
            for record in records:
                name = record["Name"]["Value"]
                fnc = record.get("FNC_VALUES") or record.get("AXIS_PTS_X")
                data_type = fnc["DataType"]["Value"]
                if not self.record_dict.get(data_type):
                    self.record_dict[data_type] = {data_type}
                self.record_dict[data_type].add(name)
    

    2.1.5 读取a2l文件中的变量信息

    a2l中的变量信息有两种,分别是标定量(CHARACTERISTIC)和观测量(MEASUREMENT)。这两种变量在a2l中的节点有所差别,但对于地址和数据类型的处理还是一样的。
    1)CHARACTERISTIC的信息读取和写入

    project = self.r_json_py.get("PROJECT")
    modules = project.get("MODULE")
    for module in modules:  # MODULE长度是1
        characteristics = module.get("CHARACTERISTIC")
        for variable in characteristics:
            # a2l中变量的属性读取
            name = variable["Name"]["Value"]
            data_type = variable["Deposit"]["Value"]
            address = int(variable["Address"].get("Value", "0"))
            # a2l中变量的属性写入示例
            variable["Deposit"]["Value"] = "UBYTE"
            variable["Address"]["Value"] = 0x12345678
    

    2)CHARACTERISTIC的信息读取和写入

    project = self.r_json_py.get("PROJECT")
    modules = project.get("MODULE")
    for module in modules:  # MODULE长度是1
        measurements = module.get("MEASUREMENT")
        for variable in measurements:
            # a2l中变量的属性
            name = variable["Name"]["Value"]
            data_type = variable["DataType"]["Value"]
            address = int(variable["ECU_ADDRESS"]["Address"].get("Value", "0"))
            # a2l中变量的属性写入示例
            variable["DataType"]["Value"] = "UBYTE"
            variable["ECU_ADDRESS"]["Address"]["Value"] = 0x12345678
    

    2.2 处理elf文件

    elf文件的格式比较复杂,最好是了解文件格式后再去做处理。找了一些非常棒的的文章和视频,可以了解下:
    elf格式:https://blog.csdn.net/GrayOnDream/article/details/124564129
    elf格式视频:https://www.bilibili.com/video/BV1u54y1Q7qf/?spm_id_from=333.337.search-card.all.click
    elf中的debug:https://blog.csdn.net/qq_42001367/article/details/107958821

    2.2.1 三方库

    使用pyelftools,参考: https://pypi.org/project/pyelftools/

    2.2.2 读取符号表中的变量信息

    符号表中的信息是有限的,只包含变量的地址信息,没有数据类型的信息。对于结构体和数组来说,其成员信息或长度是获取不到的。
    可以用readelf打印elf中的符号表信息查看:readelf -s -W Test.elf > symtab.txt
    在没有debug信息的情况下,可以用这种方式。但通常,elf中是包含debug信息的。
    示例代码如下:

    self.parse = ELFFile(open(file, 'rb'))
    for sec in self.parse.iter_sections('SHT_SYMTAB'):  # 遍历符号表,一般只有一个。
        num_symbols = sec.num_symbols()
        for index, symbol in enumerate(sec.iter_symbols()):
            print(f"\rLoading variables in ELF Symbol Table {index + 1}/{num_symbols}", end="")
            if (symbol['st_info']['type'] == 'STT_OBJECT'  # 变量
                    and symbol['st_info']['bind'] == 'STB_GLOBAL'):
                name = symbol.name
                addr = symbol.entry.st_value
                data_type = ""  # unknown
                self.vars[name] = Item(name, addr, data_type)
    

    2.2.3 读取debug中的变量信息

    debug(DWARF)中的信息很多,调试器用到的大部分信息来自这里。需要的变量信息在这里都能获取到,包括变量地址、数据类型、数据长度、结构体成员,成员地址的偏移等信息。根据这些信息,可以更新a2l中任何变量的地址和数据类型。
    不过,这部分信息比较大,读取时间比较长。
    可以用readelf打印其中信息:readelf -w -W Test.elf > debug_info.txt


    变量的信息是在编译单元(Compilation Unit,CU)中的调试节点(debug information entry,die)中,可遍历其中的内容找到需要的信息,示例代码如下:

    self.dbg_info = self.parse.get_dwarf_info()
    len_CU = len(list(self.dbg_info.iter_CUs()))
    for index, cu in enumerate(self.dbg_info.iter_CUs()):
        print(f"\rLoading variables in ELF Compilation Unit {index + 1}/{len_CU}", end="")
    
        # 跳过没有变量的CU, 节省时间。
        if not self.check_cu_has_var(cu):
            continue
    
        for index_die, die in enumerate(cu.get_top_DIE().iter_children()):
            if die.tag == "DW_TAG_variable":
                # DW_AT_location代表变量的地址,是定义变量的地方,声明的地方没有变量地址。
                if 'DW_AT_location' in die.attributes:
                    name = self.DW_AT_name(die)
                    addr = self.DW_AT_location(die)
                    dt = self.read_dt(self.read_die_value(die, 'DW_AT_type'))
                    self.vars[name] = Item(name=name, addr=addr, data_type=dt)
                    LOG.debug(f"\n{index} {name}: {self.vars[name]}")
    

    为了节省遍历时间,可检查CU关联的缩略表中是否存在变量的属性。遍历缩略表要快的多,但其中信息有限,不然直接使用缩略表。(不知道有没有其他办法提升读取速度)
    示例代码如下:

    def check_cu_has_var(cu):
        """
        查看CU中是否含有变量。
        """
        abbrev_table = cu.get_abbrev_table()
        for code in range(1, 100):  # 可能最大38
            try:
                abbrev = abbrev_table.get_abbrev(code)
                if abbrev.decl.tag == "DW_TAG_variable":
                    return True
            except KeyError:
                return False
        return False
    

    读取的名称DW_AT_name(byte类型)和地址DW_AT_location(列表)并不是想要的数据格式,需要转换一下。
    示例代码如下:

    @staticmethod
    def read_die_value(die, DW):
        """
        读取DIE中的属性值。
        """
        if DW in die.attributes:
            return die.attributes[DW].raw_value
        else:
            return None
    
    def DW_AT_name(self, die):
        """
        读取DIE的名称,从byte转为str。
        """
        return self.read_die_value(die, 'DW_AT_name').decode('gbk', errors='ignore')
    
    def DW_AT_location(self, die):
        """
        读取DIE中变量的地址信息,转成int。
        """
        value = self.read_die_value(die, 'DW_AT_location')
        if not isinstance(value, list):
            LOG.error(f"Error format/length location {value}. {die}")
            return 0
        if len(value) == 5:
            # value[0]不知道啥意思。
            addr = value[1] | (value[2] << 8) | (value[3] << 16) | (value[4] << 24)
            return addr
        else:
            LOG.error(f"Error format/length location {value}. {die}")
            return 0
    

    数据类型的读取比较麻烦,其中的类型较多,主要用到的有:

  • DW_TAG_base_type 基础类型
  • if die.tag == "DW_TAG_base_type":
        ret = self.DW_AT_name(die)
    
  • DW_TAG_structure_type 结构体类型
  • if die.tag == "DW_TAG_structure_type":
        obj.data_type_size = self.read_die_value(die, "DW_AT_byte_size")
        if die.has_children:
            for child in die.iter_children():
                member = self.read_dt(child.offset, obj)
                member.parent = obj
                obj.append(member)
        else:
            LOG.error("No children in structure_type", die)
        ret = obj
    
  • DW_TAG_member 结构体中的成员
  • if die.tag == "DW_TAG_member":
        # 结构体成员。
        obj = Item()
        obj.name = self.DW_AT_name(die)
        obj.data_type_size = int(self.read_die_value(die, "DW_AT_byte_size"))
        obj.member_offset = self.DW_AT_data_member_location(die)
        dt = self.read_dt(die.attributes['DW_AT_type'].raw_value + die.cu.cu_offset, obj)
        # 如果当前成员的数据类型是结构体,则data_type的名称是默认的。否则会出现循环引用,即obj.data_type = obj。
        if isinstance(dt, str):
            obj.data_type = dt
        ret = obj
    
  • DW_TAG_array_type 数组类型
  • if die.tag == "DW_TAG_array_type":
        dt = self.read_dt(die.attributes['DW_AT_type'].raw_value + die.cu.cu_offset, obj)
        if isinstance(dt, str):
            obj.data_type = dt
        obj.array_length = int(int(self.read_die_value(die, "DW_AT_byte_size")) / obj.data_type_size)
        ret = obj
    
  • DW_TAG_const_type和DW_TAG_volatile_type
  • if die.tag == "DW_TAG_const_type" or die.tag == "DW_TAG_volatile_type":
        ret = self.read_dt(die.attributes['DW_AT_type'].raw_value + die.cu.cu_offset, parent)
    
  • 其他的比如enum, union等,对于a2l更新用不到,可以直接忽略。
  • 以上只有die.tag==DW_TAG_base_type时,才算找到最终的基础数据类型。 过程中的die.attributes['DW_AT_type'].raw_value是下一级的索引,据此继续向下一级搜索。

    2.3 更新a2l变量

    以上从elf读到了变量的信息,而且也知道如何修改a2l中变量的信息,现在要把elf中的信息更新到a2l中。

    2.3.1 根据a2l变量名获取elf变量的信息

    对于一般类型(除了结构体和数组)的变量,直接拿到elf变量信息即可。
    但对于结构体或者数组的某个成员,是要根据变量的基地址和数据类型进行偏移的。
    示例代码如下:

    def get_elf_var_info(self, var):
        """
        获取elf中变量的地址和数据类型。
        :param var: a2l中变量的名称。包含结构体和数组类型的变量。
        :return: (int, str) 变量地址,变量C语言数据类型。
        """
        var_splits = var.split(".")
        if not self._elf_has_debug_info and len(var_splits) > 1:
            return None  # 结构体和数组变量,需要debug信息才能获取地址和数据类型。
    
        base_var = var_splits[0]  # 可能是结构体或者数组,取变量的第一级。
        info = self.elf_vars.get(base_var)  # type: ReadElf.Item
    
        if info is None:
            LOG.error(f"{var} is not found in elf_vars.")
            return None
        addr = info.addr
        elf_dt = info.data_type  # type: str | ReadElf.Item
    
        # 结构体或数组变量,继续搜索下一级。
        if len(var_splits) > 1:
            for x in var_splits[1:]:
                regex = re.compile(r"_(\d+)_")
                mo = regex.search(x)
                if mo:  # array
                    index = int(mo.group(1))
                    if index < elf_dt.array_length:
                        addr += index * elf_dt.data_type_size
                    else:
                        LOG.error(f"Index {index} out of range for {var}.")
                        return None
                else:
                    for child in elf_dt.children:  # type: ReadElf.Item
                        if child.name == x:
                            elf_dt = child
                            addr += child.member_offset
                            break
                    else:
                        LOG.error(f"{x} is not found in {var}.")
                        return None
        # 不是str,则是Var,即结构体类型。结构体类型直接返回unsigned char,a2l中对应UBYTE。
        if isinstance(elf_dt, ReadElf.Item):
            elf_dt = elf_dt.data_type
        LOG.debug(f"{var} addr: {hex(addr)}, data_type: {elf_dt}")
    
        return addr, elf_dt
    

    2.3.2 elf中的数据类型转换成a2l中的数据类型

    可以用一个字典保存elf中的基本类型(C语言的数据类型),匹配a2l的类型。
    示例如下:

    # 数据类型映射,C语言到A2L语言的映射。updating...
    mapping_c_to_a2l = {
        "char": "UBYTE",
        "unsigned char": "UBYTE",
        "signed char": "SBYTE",
        "unsigned short": "UWORD",
        "unsigned short int": "UWORD",
        "signed short": "SWORD",
        "signed short int": "SWORD",
        "unsigned long": "ULONG",
        "unsigned long int": "ULONG",
        "signed long": "SLONG",
        "signed long int": "SLONG",
        "float": "FLOAT32_IEEE",
        "double": "FLOAT64_IEEE",
    }
    

    2.3.3 更新a2l中的变量

    以MEASUREMEN为示例代码如下:

    measurements = module.get("MEASUREMENT")
    for variable in measurements:
        # a2l中变量的属性
        name = variable["Name"]["Value"]
        data_type = variable["DataType"]["Value"]
        address = int(variable["ECU_ADDRESS"]["Address"].get("Value", "0"))
        # elf中变量的属性
        ret = self.get_elf_var_info(name)
        if ret is None:
            continue
        new_addr, elf_dt = ret
        if new_addr == address:
            LOG.info(f"{name} address is the same as before.")
        else:
            variable["ECU_ADDRESS"]["Address"]["Value"] = new_addr
            LOG.info(f"{name} address is updated from {hex(address)} to {hex(new_addr)}.")
        # elf变量数据类型转换成a2l数据类型。
        if self._elf_has_debug_info:
            new_dt = mapping_c_to_a2l.get(elf_dt)
            if new_dt is None:
                LOG.error(f"Unknown C language dataType {new_dt}, please add in mapping_c_to_a2l. {name}")
                continue
            # 对比原始a2l变量类型和计算出的数据类型是否相同,不同则更新数据类型。
            a2l_dt_set = self.record_dict.get(new_dt)
            if a2l_dt_set is not None:
                if data_type not in a2l_dt_set:
                    variable["DataType"]["Value"] = iter(a2l_dt_set).__next__()
                    LOG.info(f"{name} data_type is updated from {data_type} to {iter(a2l_dt_set).__next__()}.")
            else:
                LOG.error(f"{new_dt} is not found in record_dict, update a2l RECORD_LAYOUT.{self.record_dict}")
    

    以上只是示例代码,可以参考完整代码文件:
    代码链接:gitcode

    总结

    目前上述Python脚本更新a2l大概需要半分钟左右,时间还是不短的,当然也会有其他问题和BUG。。。但毕竟多一个方式多一个选择,根据实际项目权衡合适的方式吧。
    pya2l和pyelftools没有太深入的去探索里面的功能,可能有更便捷、效率更高的方式去实现上面的功能。
    另外,也可以实现一些其他实用的功能:
    pya2l可以通过读取多个a2l文件,将其中的变量合并到一个master.a2l中。
    pyelftools可以读取的elf中section信息,可以基于此统计RAM和ROM使用情况。

    == 欢迎批评指正和探讨交流 ^_^ ==

    作者:kook 1995

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【Python】读取elf文件,更新a2l文件中变量地址

    发表回复