[FL-3386] Fast FAP Loader (#2790)

* FBT: build and add FastFAP(tm) sections
* Elf file: fast loading fap files. Really fast, like x15 times faster.
* fastfap.py: cleanup unused imports
* Toolchain: 23 version
* Elf File: remove log messages
* Scripts: fix file permissions
* FBT: explicit interpreter for fastfap invocation

Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
Sergey Gavrilov 2023-06-28 11:19:10 +03:00 committed by GitHub
parent 92c1bb83bf
commit 645a7c5989
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 338 additions and 42 deletions

View file

@ -679,7 +679,8 @@ Function,+,elements_slightly_rounded_box,void,"Canvas*, uint8_t, uint8_t, uint8_
Function,+,elements_slightly_rounded_frame,void,"Canvas*, uint8_t, uint8_t, uint8_t, uint8_t"
Function,+,elements_string_fit_width,void,"Canvas*, FuriString*, uint8_t"
Function,+,elements_text_box,void,"Canvas*, uint8_t, uint8_t, uint8_t, uint8_t, Align, Align, const char*, _Bool"
Function,+,elf_resolve_from_hashtable,_Bool,"const ElfApiInterface*, const char*, Elf32_Addr*"
Function,+,elf_resolve_from_hashtable,_Bool,"const ElfApiInterface*, uint32_t, Elf32_Addr*"
Function,+,elf_symbolname_hash,uint32_t,const char*
Function,+,empty_screen_alloc,EmptyScreen*,
Function,+,empty_screen_free,void,EmptyScreen*
Function,+,empty_screen_get_view,View*,EmptyScreen*

1 entry status name type params
679 Function + elements_slightly_rounded_frame void Canvas*, uint8_t, uint8_t, uint8_t, uint8_t
680 Function + elements_string_fit_width void Canvas*, FuriString*, uint8_t
681 Function + elements_text_box void Canvas*, uint8_t, uint8_t, uint8_t, uint8_t, Align, Align, const char*, _Bool
682 Function + elf_resolve_from_hashtable _Bool const ElfApiInterface*, const char*, Elf32_Addr* const ElfApiInterface*, uint32_t, Elf32_Addr*
683 Function + elf_symbolname_hash uint32_t const char*
684 Function + empty_screen_alloc EmptyScreen*
685 Function + empty_screen_free void EmptyScreen*
686 Function + empty_screen_get_view View* EmptyScreen*

View file

@ -808,7 +808,8 @@ Function,+,elements_slightly_rounded_box,void,"Canvas*, uint8_t, uint8_t, uint8_
Function,+,elements_slightly_rounded_frame,void,"Canvas*, uint8_t, uint8_t, uint8_t, uint8_t"
Function,+,elements_string_fit_width,void,"Canvas*, FuriString*, uint8_t"
Function,+,elements_text_box,void,"Canvas*, uint8_t, uint8_t, uint8_t, uint8_t, Align, Align, const char*, _Bool"
Function,+,elf_resolve_from_hashtable,_Bool,"const ElfApiInterface*, const char*, Elf32_Addr*"
Function,+,elf_resolve_from_hashtable,_Bool,"const ElfApiInterface*, uint32_t, Elf32_Addr*"
Function,+,elf_symbolname_hash,uint32_t,const char*
Function,+,empty_screen_alloc,EmptyScreen*,
Function,+,empty_screen_free,void,EmptyScreen*
Function,+,empty_screen_get_view,View*,EmptyScreen*

1 entry status name type params
808 Function + elements_slightly_rounded_frame void Canvas*, uint8_t, uint8_t, uint8_t, uint8_t
809 Function + elements_string_fit_width void Canvas*, FuriString*, uint8_t
810 Function + elements_text_box void Canvas*, uint8_t, uint8_t, uint8_t, uint8_t, Align, Align, const char*, _Bool
811 Function + elf_resolve_from_hashtable _Bool const ElfApiInterface*, const char*, Elf32_Addr* const ElfApiInterface*, uint32_t, Elf32_Addr*
812 Function + elf_symbolname_hash uint32_t const char*
813 Function + empty_screen_alloc EmptyScreen*
814 Function + empty_screen_free void EmptyScreen*
815 Function + empty_screen_get_view View* EmptyScreen*

View file

@ -7,27 +7,22 @@
bool elf_resolve_from_hashtable(
const ElfApiInterface* interface,
const char* name,
uint32_t hash,
Elf32_Addr* address) {
bool result = false;
const HashtableApiInterface* hashtable_interface =
static_cast<const HashtableApiInterface*>(interface);
bool result = false;
uint32_t gnu_sym_hash = elf_gnu_hash(name);
sym_entry key = {
.hash = gnu_sym_hash,
.hash = hash,
.address = 0,
};
auto find_res =
std::lower_bound(hashtable_interface->table_cbegin, hashtable_interface->table_cend, key);
if((find_res == hashtable_interface->table_cend || (find_res->hash != gnu_sym_hash))) {
if((find_res == hashtable_interface->table_cend || (find_res->hash != hash))) {
FURI_LOG_W(
TAG,
"Can't find symbol '%s' (hash %lx) @ %p!",
name,
gnu_sym_hash,
hashtable_interface->table_cbegin);
TAG, "Can't find symbol with hash %lx @ %p!", hash, hashtable_interface->table_cbegin);
result = false;
} else {
result = true;
@ -36,3 +31,7 @@ bool elf_resolve_from_hashtable(
return result;
}
uint32_t elf_symbolname_hash(const char* s) {
return elf_gnu_hash(s);
}

View file

@ -19,15 +19,17 @@ struct sym_entry {
/**
* @brief Resolver for API entries using a pre-sorted table with hashes
* @param interface pointer to HashtableApiInterface
* @param name function name
* @param hash gnu hash of function name
* @param address output for function address
* @return true if the table contains a function
*/
bool elf_resolve_from_hashtable(
const ElfApiInterface* interface,
const char* name,
uint32_t hash,
Elf32_Addr* address);
uint32_t elf_symbolname_hash(const char* s);
#ifdef __cplusplus
}
@ -48,8 +50,10 @@ struct HashtableApiInterface : public ElfApiInterface {
.hash = elf_gnu_hash(#x), .address = (uint32_t)(static_cast<ret_type(*) args_type>(x)) \
}
#define API_VARIABLE(x, var_type) \
sym_entry { .hash = elf_gnu_hash(#x), .address = (uint32_t)(&(x)), }
#define API_VARIABLE(x, var_type) \
sym_entry { \
.hash = elf_gnu_hash(#x), .address = (uint32_t)(&(x)), \
}
constexpr bool operator<(const sym_entry& k1, const sym_entry& k2) {
return k1.hash < k2.hash;

View file

@ -11,6 +11,6 @@ typedef struct ElfApiInterface {
uint16_t api_version_minor;
bool (*resolver_callback)(
const struct ElfApiInterface* interface,
const char* name,
uint32_t hash,
Elf32_Addr* address);
} ElfApiInterface;

View file

@ -2,6 +2,7 @@
#include "elf_file.h"
#include "elf_file_i.h"
#include "elf_api_interface.h"
#include "../api_hashtable/api_hashtable.h"
#define TAG "elf"
@ -9,6 +10,7 @@
#define SECTION_OFFSET(e, n) ((e)->section_table + (n) * sizeof(Elf32_Shdr))
#define IS_FLAGS_SET(v, m) (((v) & (m)) == (m))
#define RESOLVER_THREAD_YIELD_STEP 30
#define FAST_RELOCATION_VERSION 1
// #define ELF_DEBUG_LOG 1
@ -71,6 +73,7 @@ static ELFSection* elf_file_get_or_put_section(ELFFile* elf, const char* name) {
.size = 0,
.rel_count = 0,
.rel_offset = 0,
.fast_rel = NULL,
});
section_p = elf_file_get_section(elf, name);
}
@ -168,7 +171,8 @@ static ELFSection* elf_section_of(ELFFile* elf, int index) {
static Elf32_Addr elf_address_of(ELFFile* elf, Elf32_Sym* sym, const char* sName) {
if(sym->st_shndx == SHN_UNDEF) {
Elf32_Addr addr = 0;
if(elf->api_interface->resolver_callback(elf->api_interface, sName, &addr)) {
uint32_t hash = elf_symbolname_hash(sName);
if(elf->api_interface->resolver_callback(elf->api_interface, hash, &addr)) {
return addr;
}
} else {
@ -424,6 +428,7 @@ typedef enum {
SectionTypeSymTab = 1 << 3,
SectionTypeStrTab = 1 << 4,
SectionTypeDebugLink = 1 << 5,
SectionTypeFastRelData = 1 << 6,
SectionTypeValid = SectionTypeSymTab | SectionTypeStrTab,
} SectionType;
@ -505,7 +510,8 @@ static SectionType elf_preload_section(
// TODO: how to do it not by name?
// .ARM: type 0x70000001, flags SHF_ALLOC | SHF_LINK_ORDER
// .rel.ARM: type 0x9, flags SHT_REL
if(str_prefix(name, ".ARM.") || str_prefix(name, ".rel.ARM.")) {
if(str_prefix(name, ".ARM.") || str_prefix(name, ".rel.ARM.") ||
str_prefix(name, ".fast.rel.ARM.")) {
FURI_LOG_D(TAG, "Ignoring ARM section");
return SectionTypeUnused;
}
@ -536,11 +542,31 @@ static SectionType elf_preload_section(
// Load link info section
if(section_header->sh_flags & SHF_INFO_LINK) {
name = name + strlen(".rel");
if(str_prefix(name, ".rel")) {
name = name + strlen(".rel");
ELFSection* section_p = elf_file_get_or_put_section(elf, name);
section_p->rel_count = section_header->sh_size / sizeof(Elf32_Rel);
section_p->rel_offset = section_header->sh_offset;
return SectionTypeRelData;
} else {
FURI_LOG_E(TAG, "Unknown link info section '%s'", name);
return SectionTypeERROR;
}
}
// Load fast rel section
if(str_prefix(name, ".fast.rel")) {
name = name + strlen(".fast.rel");
ELFSection* section_p = elf_file_get_or_put_section(elf, name);
section_p->rel_count = section_header->sh_size / sizeof(Elf32_Rel);
section_p->rel_offset = section_header->sh_offset;
return SectionTypeRelData;
section_p->fast_rel = malloc(sizeof(ELFSection));
if(!elf_load_section_data(elf, section_p->fast_rel, section_header)) {
FURI_LOG_E(TAG, "Error loading section '%s'", name);
return SectionTypeERROR;
}
FURI_LOG_D(TAG, "Loaded fast rel section for '%s'", name);
return SectionTypeFastRelData;
}
// Load symbol table
@ -571,8 +597,90 @@ static SectionType elf_preload_section(
return SectionTypeUnused;
}
static Elf32_Addr elf_address_of_by_hash(ELFFile* elf, uint32_t hash) {
Elf32_Addr addr = 0;
if(elf->api_interface->resolver_callback(elf->api_interface, hash, &addr)) {
return addr;
}
return ELF_INVALID_ADDRESS;
}
static bool elf_relocate_fast(ELFFile* elf, ELFSection* s) {
UNUSED(elf);
const uint8_t* start = s->fast_rel->data;
const uint8_t version = *start;
if(version != FAST_RELOCATION_VERSION) {
FURI_LOG_E(TAG, "Unsupported fast relocation version %d", version);
return false;
}
start += 1;
const uint32_t records_count = *((uint32_t*)start);
start += 4;
FURI_LOG_D(TAG, "Fast relocation records count: %ld", records_count);
for(uint32_t i = 0; i < records_count; i++) {
bool is_section = (*start & (0x1 << 7)) ? true : false;
uint8_t type = *start & 0x7F;
start += 1;
uint32_t hash_or_section_index = *((uint32_t*)start);
start += 4;
uint32_t section_value = ELF_INVALID_ADDRESS;
if(is_section) {
section_value = *((uint32_t*)start);
start += 4;
}
const uint32_t offsets_count = *((uint32_t*)start);
start += 4;
FURI_LOG_D(
TAG,
"Fast relocation record %ld: is_section=%d, type=%d, hash_or_section_index=%lX, offsets_count=%ld",
i,
is_section,
type,
hash_or_section_index,
offsets_count);
Elf32_Addr address = 0;
if(is_section) {
ELFSection* symSec = elf_section_of(elf, hash_or_section_index);
if(symSec) {
address = ((Elf32_Addr)symSec->data) + section_value;
}
} else {
address = elf_address_of_by_hash(elf, hash_or_section_index);
}
if(address == ELF_INVALID_ADDRESS) {
FURI_LOG_E(TAG, "Failed to resolve address for hash %lX", hash_or_section_index);
return false;
}
for(uint32_t j = 0; j < offsets_count; j++) {
uint32_t offset = *((uint32_t*)start) & 0x00FFFFFF;
start += 3;
// FURI_LOG_I(TAG, " Fast relocation offset %ld: %ld", j, offset);
Elf32_Addr relAddr = ((Elf32_Addr)s->data) + offset;
elf_relocate_symbol(elf, relAddr, type, address);
}
}
aligned_free(s->fast_rel->data);
free(s->fast_rel);
s->fast_rel = NULL;
return true;
}
static bool elf_relocate_section(ELFFile* elf, ELFSection* section) {
if(section->rel_count) {
if(section->fast_rel) {
FURI_LOG_D(TAG, "Fast relocating section");
return elf_relocate_fast(elf, section);
} else if(section->rel_count) {
FURI_LOG_D(TAG, "Relocating section");
return elf_relocate(elf, section);
} else {
@ -630,6 +738,10 @@ void elf_file_free(ELFFile* elf) {
if(itref->value.data) {
aligned_free(itref->value.data);
}
if(itref->value.fast_rel) {
aligned_free(itref->value.fast_rel->data);
free(itref->value.fast_rel);
}
free((void*)itref->key);
}

View file

@ -13,14 +13,18 @@ DICT_DEF2(AddressCache, int, M_DEFAULT_OPLIST, Elf32_Addr, M_DEFAULT_OPLIST)
*/
typedef int32_t(entry_t)(void*);
typedef struct {
typedef struct ELFSection ELFSection;
struct ELFSection {
void* data;
uint16_t sec_idx;
Elf32_Word size;
size_t rel_count;
Elf32_Off rel_offset;
} ELFSection;
ELFSection* fast_rel;
uint16_t sec_idx;
};
DICT_DEF2(ELFSectionDict, const char*, M_CSTR_OPLIST, ELFSection, M_POD_OPLIST)

View file

@ -13,12 +13,12 @@ struct CompositeApiResolver {
static bool composite_api_resolver_callback(
const ElfApiInterface* interface,
const char* name,
uint32_t hash,
Elf32_Addr* address) {
CompositeApiResolver* resolver = (CompositeApiResolver*)interface;
for
M_EACH(interface, resolver->interfaces, ElfApiInterfaceList_t) {
if((*interface)->resolver_callback(*interface, name, address)) {
if((*interface)->resolver_callback(*interface, hash, address)) {
return true;
}
}

0
scripts/distfap.py Normal file → Executable file
View file

169
scripts/fastfap.py Executable file
View file

@ -0,0 +1,169 @@
#!/usr/bin/env python3
import hashlib
import os
import struct
import subprocess
import tempfile
from collections import defaultdict
from dataclasses import dataclass
from elftools.elf.elffile import ELFFile
from elftools.elf.relocation import RelocationSection
from elftools.elf.sections import SymbolTableSection
from fbt.sdk.hashes import gnu_sym_hash
from flipper.app import App
VERSION = 1
@dataclass
class RelData:
section: int
section_value: int
type: int
offset: int
name: str
@dataclass(frozen=True)
class UniqueRelData:
section: int
section_value: int
type: int
name: str
@dataclass
class RelSection:
name: str
oringinal_name: str
data: dict[UniqueRelData, list[int]]
def serialize_relsection_data(data: dict[UniqueRelData, list[int]]) -> bytes:
result = struct.pack("<B", VERSION)
result += struct.pack("<I", len(data))
for unique, values in data.items():
if unique.section > 0:
result += struct.pack("<B", (1 << 7) | unique.type & 0x7F)
result += struct.pack("<I", unique.section)
result += struct.pack("<I", unique.section_value)
else:
result += struct.pack("<B", (0 << 7) | unique.type & 0x7F)
result += struct.pack("<I", gnu_sym_hash(unique.name))
result += struct.pack("<I", len(values))
for offset in values:
result += struct.pack(
"<BBB", offset & 0xFF, (offset >> 8) & 0xFF, (offset >> 16) & 0xFF
)
return result
class Main(App):
def init(self):
self.parser.add_argument("fap_src_path", help="App file to upload")
self.parser.add_argument("objcopy_path", help="Objcopy path")
self.parser.set_defaults(func=self.process)
def process(self):
fap_path = self.args.fap_src_path
objcopy_path = self.args.objcopy_path
sections: list[RelSection] = []
with open(fap_path, "rb") as f:
elf_file = ELFFile(f)
relocation_sections: list[RelocationSection] = []
symtab_section: SymbolTableSection | None = None
for section in elf_file.iter_sections():
if isinstance(section, RelocationSection):
relocation_sections.append(section)
if isinstance(section, SymbolTableSection):
symtab_section = section
if not symtab_section:
self.logger.error("No symbol table found")
return 1
if not relocation_sections:
self.logger.info("No relocation sections found")
return 0
for section in relocation_sections:
section_relocations: list[RelData] = []
for relocation in section.iter_relocations():
symbol_id: int = relocation.entry["r_info_sym"]
offset: int = relocation.entry["r_offset"]
type: int = relocation.entry["r_info_type"]
symbol = symtab_section.get_symbol(symbol_id)
section_index: int = symbol["st_shndx"]
section_value: int = symbol["st_value"]
if section_index == "SHN_UNDEF":
section_index = 0
section_relocations.append(
RelData(section_index, section_value, type, offset, symbol.name)
)
unique_relocations: dict[UniqueRelData, list[int]] = defaultdict(list)
for relocation in section_relocations:
unique = UniqueRelData(
relocation.section,
relocation.section_value,
relocation.type,
relocation.name,
)
unique_relocations[unique].append(relocation.offset)
section_name = section.name
if section_name.startswith(".rel"):
section_name = ".fast.rel" + section_name[4:]
else:
self.logger.error(
"Unknown relocation section name: %s", section_name
)
return 1
sections.append(
RelSection(section_name, section.name, unique_relocations)
)
with tempfile.TemporaryDirectory() as temp_dir:
for section in sections:
data = serialize_relsection_data(section.data)
hash_name = hashlib.md5(section.name.encode()).hexdigest()
filename = f"{temp_dir}/{hash_name}.bin"
if os.path.isfile(filename):
self.logger.error(f"File {filename} already exists")
return 1
with open(filename, "wb") as f:
f.write(data)
exit_code = subprocess.run(
[
objcopy_path,
"--add-section",
f"{section.name}={filename}",
fap_path,
],
check=True,
)
if exit_code.returncode != 0:
self.logger.error("objcopy failed")
return 1
return 0
if __name__ == "__main__":
Main()()

View file

@ -1,4 +1,5 @@
from typing import List
from .hashes import gnu_sym_hash
from cxxheaderparser.parser import CxxParser
from . import (
@ -72,13 +73,6 @@ class SymbolManager:
self.api.headers.add(ApiHeader(header))
def gnu_sym_hash(name: str):
h = 0x1505
for c in name:
h = (h << 5) + h + ord(c)
return str(hex(h))[-8:]
class SdkCollector:
def __init__(self):
self.symbol_manager = SymbolManager()

View file

@ -0,0 +1,5 @@
def gnu_sym_hash(name: str) -> int:
h = 0x1505
for c in name:
h = ((h << 5) + h + ord(c)) & 0xFFFFFFFF
return h

View file

@ -384,10 +384,16 @@ def generate_embed_app_metadata_actions(source, target, env, for_signature):
"${SOURCES} ${TARGET}"
)
actions.append(
Action(
objcopy_str,
"$APPMETAEMBED_COMSTR",
actions.extend(
(
Action(
objcopy_str,
"$APPMETAEMBED_COMSTR",
),
Action(
"${PYTHON3} ${FBT_SCRIPT_DIR}/fastfap.py ${TARGET} ${OBJCOPY}",
"$FASTFAP_COMSTR",
),
)
)
@ -450,6 +456,7 @@ def generate(env, **kw):
APPMETA_COMSTR="\tAPPMETA\t${TARGET}",
APPFILE_COMSTR="\tAPPFILE\t${TARGET}",
APPMETAEMBED_COMSTR="\tFAP\t${TARGET}",
FASTFAP_COMSTR="\tFASTFAP\t${TARGET}",
APPCHECK_COMSTR="\tAPPCHK\t${SOURCE}",
)

0
scripts/fwsize.py Normal file → Executable file
View file

0
scripts/get_env.py Normal file → Executable file
View file

0
scripts/runfap.py Normal file → Executable file
View file

0
scripts/sconsdist.py Normal file → Executable file
View file

0
scripts/selfupdate.py Normal file → Executable file
View file

0
scripts/slideshow.py Normal file → Executable file
View file

View file

@ -13,7 +13,7 @@ if not ["%FBT_NOENV%"] == [""] (
exit /b 0
)
set "FLIPPER_TOOLCHAIN_VERSION=21"
set "FLIPPER_TOOLCHAIN_VERSION=22"
if ["%FBT_TOOLCHAIN_PATH%"] == [""] (
set "FBT_TOOLCHAIN_PATH=%FBT_ROOT%"

View file

@ -4,7 +4,7 @@
# public variables
DEFAULT_SCRIPT_PATH="$(pwd -P)";
FBT_TOOLCHAIN_VERSION="${FBT_TOOLCHAIN_VERSION:-"21"}";
FBT_TOOLCHAIN_VERSION="${FBT_TOOLCHAIN_VERSION:-"22"}";
if [ -z ${FBT_TOOLCHAIN_PATH+x} ] ; then
FBT_TOOLCHAIN_PATH_WAS_SET=0;

0
scripts/version.py Normal file → Executable file
View file