Estabilidade da ABI

A estabilidade da Interface binária do aplicativo (ABI) é um pré-requisito de atualizações somente de framework, porque os módulos do fornecedor podem depender das bibliotecas compartilhadas do kit de desenvolvimento nativo do fornecedor (VNDK, na sigla em inglês) que residem na partição do sistema. Em uma versão do Android, as bibliotecas compartilhadas do VNDK recém-criadas precisam ser compatíveis com a ABI das bibliotecas compartilhadas do VNDK lançadas anteriormente para que os módulos do fornecedor funcionem com essas bibliotecas sem recompilação e sem erros de execução. Entre as versões do Android, as bibliotecas do VNDK podem ser alteradas, e não há garantias de ABI.

Para ajudar a garantir a compatibilidade da ABI, o Android 9 inclui um verificador de cabeçalho ABI, conforme descrito nas seções a seguir.

Sobre a conformidade com o VNDK e a ABI

O VNDK é um conjunto restritivo de bibliotecas que os módulos do fornecedor podem vincular e que permitem atualizações somente de framework. A conformidade com a ABI se refere à capacidade de uma versão mais recente de uma biblioteca compartilhada de funcionar conforme o esperado com um módulo vinculado dinamicamente a ela (ou seja, funciona como uma versão mais antiga da biblioteca).

Sobre os símbolos exportados

Um símbolo exportado (também conhecido como símbolo global) refere-se a um símbolo que satisfaz todos os itens a seguir:

  • Exportado pelos cabeçalhos públicos de uma biblioteca compartilhada.
  • Aparece na tabela .dynsym do arquivo .so correspondente à biblioteca compartilhada.
  • Tem vinculação WEAK ou GLOBAL.
  • A visibilidade é DEFAULT ou PROTECTED.
  • O índice da seção não está UNDEFINED.
  • O tipo é FUNC ou OBJECT.

Os cabeçalhos públicos de uma biblioteca compartilhada são definidos como os cabeçalhos disponíveis para outras bibliotecas/binários pelos atributos export_include_dirs, export_header_lib_headers, export_static_lib_headers, export_shared_lib_headers e export_generated_headers nas definições de Android.bp do módulo correspondente à biblioteca compartilhada.

Sobre os tipos alcançáveis

Um tipo alcançável é qualquer tipo integrado ou definido pelo usuário em C/C++ que seja alcançável diretamente ou indiretamente por um símbolo exportado E exportado por cabeçalhos públicos. Por exemplo, libfoo.so tem a função Foo, que é um símbolo exportado encontrado na tabela .dynsym. A biblioteca libfoo.so inclui o seguinte:

foo_exported.h foo.private.h
typedef struct foo_private foo_private_t;

typedef struct foo {
  int m1;
  int *m2;
  foo_private_t *mPfoo;
} foo_t;

typedef struct bar {
  foo_t mfoo;
} bar_t;

bool Foo(int id, bar_t *bar_ptr);
typedef struct foo_private {
  int m1;
  float mbar;
} foo_private_t;
Android.bp
cc_library {
  name : libfoo,
  vendor_available: true,
  vndk {
    enabled : true,
  }
  srcs : ["src/*.cpp"],
  export_include_dirs : [
    "exported"
  ],
}
Tabela .dynsym
Num Value Size Type Bind Vis Ndx Name
1 0 0 FUNC GLOB DEF UND dlerror@libc
2 1ce0 20 FUNC GLOB DEF 12 Foo

No caso de Foo, os tipos acessíveis diretos/indiretos incluem:

Tipo Descrição
bool Retorne o tipo Foo.
int Tipo do primeiro parâmetro Foo.
bar_t * Tipo do segundo parâmetro Foo. Por meio de bar_t *, bar_t é exportado por foo_exported.h.

bar_t contém um membro mfoo, do tipo foo_t, que é exportado por foo_exported.h, o que resulta na exportação de mais tipos:
  • int : é o tipo de m1.
  • int * : é o tipo de m2.
  • foo_private_t * : é o tipo de mPfoo.

No entanto, foo_private_t NÃO pode ser acessado porque não é exportado por foo_exported.h. foo_private_t * é opaco, portanto, as mudanças feitas em foo_private_t são permitidas.

Uma explicação semelhante pode ser dada para tipos acessíveis por especificadores de classe base e parâmetros de modelo.

Garantir a conformidade com a ABI

A conformidade com a ABI precisa ser garantida para as bibliotecas marcadas como vendor_available: true e vndk.enabled: true nos arquivos Android.bp correspondentes. Exemplo:

cc_library {
    name: "libvndk_example",
    vendor_available: true,
    vndk: {
        enabled: true,
    }
}

Para tipos de dados acessíveis direta ou indiretamente por uma função exportada, as seguintes alterações em uma biblioteca são classificadas como quebrando a ABI:

Tipo de dado Descrição
Estruturas e classes
  • Mude o tamanho do tipo de classe ou de struct.
  • Classes de base
    • Adicione ou remova classes base.
    • Adicione ou remova classes base virtualmente herdadas.
    • Mudar a ordem das classes básicas.
  • Funções participantes
    • Remover funções de membro*.
    • Adicione ou remova argumentos de funções de membros.
    • Altere os tipos de argumento ou os tipos de retorno das funções de membro*.
    • Mudar o layout da tabela virtual.
  • Membros de dados
    • Remova membros de dados estáticos.
    • Adicione ou remova membros de dados não estáticos.
    • Altere os tipos de membros de dados.
    • Altere os deslocamentos para membros de dados não estáticos**.
    • Mude os qualificadores const, volatile e/ou restricted dos membros de dados***.
    • Faça downgrade dos especificadores de acesso dos membros de dados***.
  • Mude os argumentos do modelo.
União
  • Adicione ou remova participantes de dados.
  • Mude o tamanho do tipo de união.
  • Mude os tipos de membros de dados.
Enumerações
  • Mude o tipo subjacente.
  • Mudar os nomes dos enumeradores.
  • Mudar os valores dos enumeradores.
Símbolos globais
  • Remova os símbolos exportados por cabeçalhos públicos.
  • Para símbolos globais do tipo FUNC
    • Adicione ou remova argumentos.
    • Mude os tipos de argumentos.
    • Mude o tipo de retorno.
    • Faça downgrade do especificador de acesso***.
  • Para símbolos globais do tipo OBJECT
    • Mude o tipo C/C++ correspondente.
    • Faça downgrade do especificador de acesso***.

* As funções de membro públicas e privadas não podem ser alteradas ou removidas, porque as funções inline públicas podem se referir a funções de membro privadas. As referências de símbolo a funções de membro privadas podem ser mantidas em binários do autor da chamada. Alterar ou remover funções de membro privadas de bibliotecas compartilhadas pode resultar em binários incompatíveis com versões anteriores.

** Os deslocamentos em membros de dados públicos ou privados não podem ser alterados porque as funções in-line podem se referir a esses membros de dados no corpo da função. Mudar os deslocamentos de membros de dados pode resultar em binários incompatíveis com versões anteriores.

*** Embora eles não mudem o layout de memória do tipo, existem diferenças semânticas que podem fazer com que as bibliotecas não funcionem como esperado.

Usar ferramentas de compliance com a ABI

Quando uma biblioteca do VNDK é criada, a ABI dela é comparada com a referência de ABI correspondente para a versão do VNDK que está sendo criada. Os despejos da ABI de referência estão localizados em:

${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/<PLATFORM_VNDK_VERSION>/<BINDER_BITNESS>/<ARCH>/source-based

Por exemplo, ao criar libfoo para x86 no nível 27 da API, a ABI inferida de libfoo é comparada com a referência em:

${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/27/64/x86/source-based/libfoo.so.lsdump

Erro de falha da ABI

Em violações de ABI, o registro de build mostra avisos com o tipo de aviso e um caminho para o relatório abi-diff. Por exemplo, se a ABI de libbinder tiver uma mudança incompatível, o sistema de compilação vai gerar um erro com uma mensagem semelhante a esta:

*****************************************************
error: VNDK library: libbinder.so's ABI has INCOMPATIBLE CHANGES
Please check compatibility report at:
out/soong/.intermediates/frameworks/native/libs/binder/libbinder/android_arm64_armv8-a_cortex-a73_vendor_shared/libbinder.so.abidiff
******************************************************
---- Please update abi references by running
platform/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l libbinder ----

Criar verificações de ABI da biblioteca do VNDK

Quando uma biblioteca do VNDK é criada:

  1. O header-abi-dumper processa os arquivos de origem compilados para criar a biblioteca VNDK (os próprios arquivos de origem da biblioteca, bem como os arquivos de origem herdados por dependências transitivas estáticas), para produzir arquivos .sdump que correspondam a cada origem.
    Criação de sdump
    Figura 1. Como criar os arquivos .sdump
  2. Em seguida, header-abi-linker processa os arquivos .sdump usando um script de versão fornecido a ele ou o arquivo .so correspondente à biblioteca compartilhada para produzir um arquivo .lsdump que registra todas as informações da ABI correspondentes à biblioteca compartilhada.
    Criação de lsdump
    Figura 2. Como criar o arquivo .lsdump
  3. header-abi-diff compara o arquivo .lsdump com um arquivo .lsdump de referência para produzir um relatório de diferenças que descreve as diferenças nas ABIs das duas bibliotecas.
    Criação de diff de abi
    Figura 3. Como criar o relatório de diferenças

header-abi-dumper

A ferramenta header-abi-dumper analisa um arquivo de origem C/C++ e despeja a ABI inferida desse arquivo de origem em um arquivo intermediário. O sistema de build executa header-abi-dumper em todos os arquivos de origem compilados e também cria uma biblioteca que inclui os arquivos de origem de dependências transitivas.

Entradas
  • Um arquivo de origem C/C++
  • Diretórios de inclusão exportados
  • Flags do compilador
Saída Um arquivo que descreve a ABI do arquivo de origem. Por exemplo, foo.sdump representa a ABI de foo.cpp.

No momento, os arquivos .sdump estão no formato JSON, que não tem garantia de estabilidade em versões futuras. Dessa forma, a formatação do arquivo .sdump precisa ser considerada um detalhe de implementação do sistema de build.

Por exemplo, libfoo.so tem o seguinte arquivo de origem foo.cpp:

#include <stdio.h>
#include <foo_exported.h>

bool Foo(int id, bar_t *bar_ptr) {
    if (id > 0 && bar_ptr->mfoo.m1 > 0) {
        return true;
    }
    return false;
}

É possível usar header-abi-dumper para gerar um arquivo .sdump intermediário que represente a ABI apresentada pelo arquivo de origem usando:

$ header-abi-dumper foo.cpp -I exported -o foo.sdump -- -I exported -x c++

Esse comando instrui o header-abi-dumper a analisar foo.cpp com as flags do compilador após -- e emite as informações da ABI que são exportadas pelos cabeçalhos públicos no diretório exported. O valor a seguir é foo.sdump gerado por header-abi-dumper:

{
 "array_types" : [],
 "builtin_types" :
 [
  {
   "alignment" : 4,
   "is_integral" : true,
   "linker_set_key" : "_ZTIi",
   "name" : "int",
   "referenced_type" : "_ZTIi",
   "self_type" : "_ZTIi",
   "size" : 4
  }
 ],
 "elf_functions" : [],
 "elf_objects" : [],
 "enum_types" : [],
 "function_types" : [],
 "functions" :
 [
  {
   "function_name" : "FooBad",
   "linker_set_key" : "_Z6FooBadiP3foo",
   "parameters" :
   [
    {
     "referenced_type" : "_ZTIi"
    },
    {
     "referenced_type" : "_ZTIP3foo"
    }
   ],
   "return_type" : "_ZTI3bar",
   "source_file" : "exported/foo_exported.h"
  }
 ],
 "global_vars" : [],
 "lvalue_reference_types" : [],
 "pointer_types" :
 [
  {
   "alignment" : 8,
   "linker_set_key" : "_ZTIP11foo_private",
   "name" : "foo_private *",
   "referenced_type" : "_ZTI11foo_private",
   "self_type" : "_ZTIP11foo_private",
   "size" : 8,
   "source_file" : "exported/foo_exported.h"
  },
  {
   "alignment" : 8,
   "linker_set_key" : "_ZTIP3foo",
   "name" : "foo *",
   "referenced_type" : "_ZTI3foo",
   "self_type" : "_ZTIP3foo",
   "size" : 8,
   "source_file" : "exported/foo_exported.h"
  },
  {
   "alignment" : 8,
   "linker_set_key" : "_ZTIPi",
   "name" : "int *",
   "referenced_type" : "_ZTIi",
   "self_type" : "_ZTIPi",
   "size" : 8,
   "source_file" : "exported/foo_exported.h"
  }
 ],
 "qualified_types" : [],
 "record_types" :
 [
  {
   "alignment" : 8,
   "fields" :
   [
    {
     "field_name" : "mfoo",
     "referenced_type" : "_ZTI3foo"
    }
   ],
   "linker_set_key" : "_ZTI3bar",
   "name" : "bar",
   "referenced_type" : "_ZTI3bar",
   "self_type" : "_ZTI3bar",
   "size" : 24,
   "source_file" : "exported/foo_exported.h"
  },
  {
   "alignment" : 8,
   "fields" :
   [
    {
     "field_name" : "m1",
     "referenced_type" : "_ZTIi"
    },
    {
     "field_name" : "m2",
     "field_offset" : 64,
     "referenced_type" : "_ZTIPi"
    },
    {
     "field_name" : "mPfoo",
     "field_offset" : 128,
     "referenced_type" : "_ZTIP11foo_private"
    }
   ],
   "linker_set_key" : "_ZTI3foo",
   "name" : "foo",
   "referenced_type" : "_ZTI3foo",
   "self_type" : "_ZTI3foo",
   "size" : 24,
   "source_file" : "exported/foo_exported.h"
  }
 ],
 "rvalue_reference_types" : []
}

foo.sdump contém informações da ABI exportadas pelo arquivo de origem foo.cpp e os cabeçalhos públicos, por exemplo,

  • record_types. Consulte estruturas, uniões ou classes definidas nos cabeçalhos públicos. Cada tipo de registro tem informações sobre os campos, o tamanho, o especificador de acesso, o arquivo de cabeçalho em que ele é definido e outros atributos.
  • pointer_types. Consulte os tipos de ponteiro referenciados direta/indiretamente pelos registros/funções exportados nos cabeçalhos públicos, além do tipo para o qual o ponteiro aponta (por meio do campo referenced_type em type_info). Informações semelhantes são registradas no arquivo .sdump para tipos qualificados, tipos C/C++ integrados, tipos de matriz e tipos de referência lvalue e rvalue. Essas informações permitem a comparação recursiva.
  • functions: representa funções exportadas por cabeçalhos públicos. Eles também têm informações sobre o nome modificado da função, o tipo de retorno, os tipos de parâmetros, o especificador de acesso e outros atributos.

header-abi-linker

A ferramenta header-abi-linker usa os arquivos intermediários produzidos por header-abi-dumper como entrada e os vincula:

Entradas
  • Arquivos intermediários produzidos por header-abi-dumper
  • Arquivo de script/mapa da versão (opcional)
  • Arquivo .so da biblioteca compartilhada
  • Diretórios de inclusão exportados
Saída Um arquivo que descreve a ABI de uma biblioteca compartilhada. Por exemplo, libfoo.so.lsdump representa a ABI de libfoo.

A ferramenta mescla os gráficos de tipo em todos os arquivos intermediários fornecidos, considerando as diferenças de uma definição (tipos definidos pelo usuário em diferentes unidades de tradução com o mesmo nome totalmente qualificado, podem ser semanticamente diferentes) entre unidades de tradução. Em seguida, a ferramenta analisa um script de versão ou a tabela .dynsym da biblioteca compartilhada (arquivo .so) para criar uma lista dos símbolos exportados.

Por exemplo, libfoo consiste em foo.cpp e bar.cpp. header-abi-linker poderia ser invocado para criar o despejo completo da ABI vinculado de libfoo da seguinte maneira:

header-abi-linker -I exported foo.sdump bar.sdump \
                  -o libfoo.so.lsdump \
                  -so libfoo.so \
                  -arch arm64 -api current

Exemplo de saída de comando em libfoo.so.lsdump:

{
 "array_types" : [],
 "builtin_types" :
 [
  {
   "alignment" : 1,
   "is_integral" : true,
   "is_unsigned" : true,
   "linker_set_key" : "_ZTIb",
   "name" : "bool",
   "referenced_type" : "_ZTIb",
   "self_type" : "_ZTIb",
   "size" : 1
  },
  {
   "alignment" : 4,
   "is_integral" : true,
   "linker_set_key" : "_ZTIi",
   "name" : "int",
   "referenced_type" : "_ZTIi",
   "self_type" : "_ZTIi",
   "size" : 4
  }
 ],
 "elf_functions" :
 [
  {
   "name" : "_Z3FooiP3bar"
  },
  {
   "name" : "_Z6FooBadiP3foo"
  }
 ],
 "elf_objects" : [],
 "enum_types" : [],
 "function_types" : [],
 "functions" :
 [
  {
   "function_name" : "Foo",
   "linker_set_key" : "_Z3FooiP3bar",
   "parameters" :
   [
    {
     "referenced_type" : "_ZTIi"
    },
    {
     "referenced_type" : "_ZTIP3bar"
    }
   ],
   "return_type" : "_ZTIb",
   "source_file" : "exported/foo_exported.h"
  },
  {
   "function_name" : "FooBad",
   "linker_set_key" : "_Z6FooBadiP3foo",
   "parameters" :
   [
    {
     "referenced_type" : "_ZTIi"
    },
    {
     "referenced_type" : "_ZTIP3foo"
    }
   ],
   "return_type" : "_ZTI3bar",
   "source_file" : "exported/foo_exported.h"
  }
 ],
 "global_vars" : [],
 "lvalue_reference_types" : [],
 "pointer_types" :
 [
  {
   "alignment" : 8,
   "linker_set_key" : "_ZTIP11foo_private",
   "name" : "foo_private *",
   "referenced_type" : "_ZTI11foo_private",
   "self_type" : "_ZTIP11foo_private",
   "size" : 8,
   "source_file" : "exported/foo_exported.h"
  },
  {
   "alignment" : 8,
   "linker_set_key" : "_ZTIP3bar",
   "name" : "bar *",
   "referenced_type" : "_ZTI3bar",
   "self_type" : "_ZTIP3bar",
   "size" : 8,
   "source_file" : "exported/foo_exported.h"
  },
  {
   "alignment" : 8,
   "linker_set_key" : "_ZTIP3foo",
   "name" : "foo *",
   "referenced_type" : "_ZTI3foo",
   "self_type" : "_ZTIP3foo",
   "size" : 8,
   "source_file" : "exported/foo_exported.h"
  },
  {
   "alignment" : 8,
   "linker_set_key" : "_ZTIPi",
   "name" : "int *",
   "referenced_type" : "_ZTIi",
   "self_type" : "_ZTIPi",
   "size" : 8,
   "source_file" : "exported/foo_exported.h"
  }
 ],
 "qualified_types" : [],
 "record_types" :
 [
  {
   "alignment" : 8,
   "fields" :
   [
    {
     "field_name" : "mfoo",
     "referenced_type" : "_ZTI3foo"
    }
   ],
   "linker_set_key" : "_ZTI3bar",
   "name" : "bar",
   "referenced_type" : "_ZTI3bar",
   "self_type" : "_ZTI3bar",
   "size" : 24,
   "source_file" : "exported/foo_exported.h"
  },
  {
   "alignment" : 8,
   "fields" :
   [
    {
     "field_name" : "m1",
     "referenced_type" : "_ZTIi"
    },
    {
     "field_name" : "m2",
     "field_offset" : 64,
     "referenced_type" : "_ZTIPi"
    },
    {
     "field_name" : "mPfoo",
     "field_offset" : 128,
     "referenced_type" : "_ZTIP11foo_private"
    }
   ],
   "linker_set_key" : "_ZTI3foo",
   "name" : "foo",
   "referenced_type" : "_ZTI3foo",
   "self_type" : "_ZTI3foo",
   "size" : 24,
   "source_file" : "exported/foo_exported.h"
  }
 ],
 "rvalue_reference_types" : []
}

A ferramenta header-abi-linker:

  • Vincula os arquivos .sdump fornecidos a ele (foo.sdump e bar.sdump), filtrando as informações de ABI que não estão presentes nos cabeçalhos presentes no diretório: exported.
  • Analisa libfoo.so e coleta informações sobre os símbolos exportados pela biblioteca por meio da tabela .dynsym.
  • _Z3FooiP3bar e _Z6FooBadiP3foo foram adicionados

libfoo.so.lsdump é o despejo ABI gerado final de libfoo.so.

header-abi-diff

A ferramenta header-abi-diff compara dois arquivos .lsdump que representam a ABI de duas bibliotecas e produz um relatório de diferenças que indica as diferenças entre as duas ABIs.

Entradas
  • Arquivo .lsdump que representa a ABI de uma biblioteca compartilhada antiga.
  • Arquivo .lsdump que representa a ABI de uma nova biblioteca compartilhada.
Saída Um relatório de diferença que mostra as diferenças nas ABIs oferecidas pelas duas bibliotecas compartilhadas comparadas.

O arquivo de diferença da ABI está no formato de texto protobuf. O formato está sujeito a mudanças em versões futuras.

Por exemplo, você tem duas versões de libfoo: libfoo_old.so e libfoo_new.so. Em libfoo_new.so, em bar_t, você muda o tipo de mfoo de foo_t para foo_t *. Como bar_t é um tipo acessível, ele precisa ser sinalizado como uma alteração interruptiva de ABI por header-abi-diff.

Para executar header-abi-diff:

header-abi-diff -old libfoo_old.so.lsdump \
                -new libfoo_new.so.lsdump \
                -arch arm64 \
                -o libfoo.so.abidiff \
                -lib libfoo

Exemplo de saída de comando em libfoo.so.abidiff:

lib_name: "libfoo"
arch: "arm64"
record_type_diffs {
  name: "bar"
  type_stack: "Foo-> bar *->bar "
  type_info_diff {
    old_type_info {
      size: 24
      alignment: 8
    }
    new_type_info {
      size: 8
      alignment: 8
    }
  }
  fields_diff {
    old_field {
      referenced_type: "foo"
      field_offset: 0
      field_name: "mfoo"
      access: public_access
    }
    new_field {
      referenced_type: "foo *"
      field_offset: 0
      field_name: "mfoo"
      access: public_access
    }
  }
}

O libfoo.so.abidiff contém um relatório de todas as mudanças de ABI em libfoo. A mensagem record_type_diffs indica que um registro foi alterado e lista as mudanças incompatíveis, que incluem:

  • O tamanho do registro muda de 24 bytes para 8 bytes.
  • O tipo de campo de mfoo mudou de foo para foo * (todos os tipos definidos pelo usuário foram removidos).

O campo type_stack indica como header-abi-diff alcançou o tipo que mudou (bar). Esse campo pode ser interpretado como Foo é uma função exportada que recebe bar * como parâmetro, que aponta para bar, que foi exportado e alterado.

Aplicar ABI e API

Para aplicar a ABI e a API das bibliotecas compartilhadas do VNDK, as referências da ABI precisam ser verificadas em ${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/. Para criar essas referências, execute o seguinte comando:

${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py

Depois de criar as referências, qualquer mudança feita no código-fonte que resulte em uma mudança de ABI/API incompatível em uma biblioteca do VNDK agora resulta em um erro de build.

Para atualizar as referências de ABI de bibliotecas específicas, execute o seguinte comando:

${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l <lib1> -l <lib2>

Por exemplo, para atualizar as referências da ABI libbinder, execute:

${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l libbinder