【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”手册中的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())
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"))
注意 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
数据类型的读取比较麻烦,其中的类型较多,主要用到的有:
if die.tag == "DW_TAG_base_type":
ret = self.DW_AT_name(die)
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
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
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
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)
以上只有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