You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
473 lines
13 KiB
473 lines
13 KiB
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
// -*- Mode: C++ -*-
|
|
//
|
|
// Copyright (C) 2017-2020 Red Hat, Inc.
|
|
//
|
|
// Author: Dodji Seketeli
|
|
|
|
/// @file
|
|
///
|
|
/// The source code of the Kernel Module Interface Diff tool.
|
|
|
|
#include <sys/types.h>
|
|
#include <dirent.h>
|
|
#include <fts.h>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <iostream>
|
|
|
|
#include "abg-config.h"
|
|
#include "abg-tools-utils.h"
|
|
#include "abg-corpus.h"
|
|
#include "abg-dwarf-reader.h"
|
|
#include "abg-reader.h"
|
|
#include "abg-comparison.h"
|
|
|
|
using std::string;
|
|
using std::vector;
|
|
using std::ostream;
|
|
using std::cout;
|
|
using std::cerr;
|
|
|
|
using namespace abigail::tools_utils;
|
|
using namespace abigail::dwarf_reader;
|
|
using namespace abigail::ir;
|
|
|
|
using abigail::comparison::diff_context_sptr;
|
|
using abigail::comparison::diff_context;
|
|
using abigail::comparison::translation_unit_diff_sptr;
|
|
using abigail::comparison::corpus_diff;
|
|
using abigail::comparison::corpus_diff_sptr;
|
|
using abigail::comparison::compute_diff;
|
|
using abigail::comparison::get_default_harmless_categories_bitmap;
|
|
using abigail::comparison::get_default_harmful_categories_bitmap;
|
|
using abigail::suppr::suppression_sptr;
|
|
using abigail::suppr::suppressions_type;
|
|
using abigail::suppr::read_suppressions;
|
|
using abigail::tools_utils::guess_file_type;
|
|
using abigail::tools_utils::file_type;
|
|
using abigail::xml_reader::read_corpus_group_from_native_xml_file;
|
|
|
|
/// The options of this program.
|
|
struct options
|
|
{
|
|
bool display_usage;
|
|
bool display_version;
|
|
bool verbose;
|
|
bool missing_operand;
|
|
bool leaf_changes_only;
|
|
bool show_hexadecimal_values;
|
|
bool show_offsets_sizes_in_bits;
|
|
bool show_impacted_interfaces;
|
|
string wrong_option;
|
|
string kernel_dist_root1;
|
|
string kernel_dist_root2;
|
|
string vmlinux1;
|
|
string vmlinux2;
|
|
vector<string> kabi_whitelist_paths;
|
|
vector<string> suppression_paths;
|
|
suppressions_type read_time_supprs;
|
|
suppressions_type diff_time_supprs;
|
|
shared_ptr<char> di_root_path1;
|
|
shared_ptr<char> di_root_path2;
|
|
|
|
options()
|
|
: display_usage(),
|
|
display_version(),
|
|
verbose(),
|
|
missing_operand(),
|
|
leaf_changes_only(true),
|
|
show_hexadecimal_values(true),
|
|
show_offsets_sizes_in_bits(false),
|
|
show_impacted_interfaces(false)
|
|
{}
|
|
}; // end struct options.
|
|
|
|
/// Display the usage of the program.
|
|
///
|
|
/// @param prog_name the name of this program.
|
|
///
|
|
/// @param out the output stream the usage stream is sent to.
|
|
static void
|
|
display_usage(const string& prog_name, ostream& out)
|
|
{
|
|
emit_prefix(prog_name, out)
|
|
<< "usage: " << prog_name << " [options] kernel-modules-dir1 kernel-modules-dir2\n"
|
|
<< " where options can be:\n"
|
|
<< " --help|-h display this message\n"
|
|
<< " --version|-v display program version information and exit\n"
|
|
<< " --verbose display verbose messages\n"
|
|
<< " --debug-info-dir1|--d1 <path> the root for the debug info of "
|
|
"the first kernel\n"
|
|
<< " --debug-info-dir2|--d2 <path> the root for the debug info of "
|
|
"the second kernel\n"
|
|
<< " --vmlinux1|--l1 <path> the path to the first vmlinux\n"
|
|
<< " --vmlinux2|--l2 <path> the path to the second vmlinux\n"
|
|
<< " --suppressions|--suppr <path> specify a suppression file\n"
|
|
<< " --kmi-whitelist|-w <path> path to a kernel module interface "
|
|
"whitelist\n"
|
|
<< " --impacted-interfaces|-i show interfaces impacted by ABI changes\n"
|
|
<< " --full-impact|-f show the full impact of changes on top-most "
|
|
"interfaces\n"
|
|
<< " --show-bytes show size and offsets in bytes\n"
|
|
<< " --show-bits show size and offsets in bits\n"
|
|
<< " --show-hex show size and offset in hexadecimal\n"
|
|
<< " --show-dec show size and offset in decimal\n";
|
|
}
|
|
|
|
/// Parse the command line of the program.
|
|
///
|
|
/// @param argc the number of arguments on the command line, including
|
|
/// the program name.
|
|
///
|
|
/// @param argv the arguments on the command line, including the
|
|
/// program name.
|
|
///
|
|
/// @param opts the options resulting from the command line parsing.
|
|
///
|
|
/// @return true iff the command line parsing went fine.
|
|
bool
|
|
parse_command_line(int argc, char* argv[], options& opts)
|
|
{
|
|
if (argc < 2)
|
|
return false;
|
|
|
|
for (int i = 1; i < argc; ++i)
|
|
{
|
|
if (argv[i][0] != '-')
|
|
{
|
|
if (opts.kernel_dist_root1.empty())
|
|
opts.kernel_dist_root1 = argv[i];
|
|
else if (opts.kernel_dist_root2.empty())
|
|
opts.kernel_dist_root2 = argv[i];
|
|
else
|
|
return false;
|
|
}
|
|
else if (!strcmp(argv[i], "--verbose"))
|
|
opts.verbose = true;
|
|
else if (!strcmp(argv[i], "--version")
|
|
|| !strcmp(argv[i], "-v"))
|
|
{
|
|
opts.display_version = true;
|
|
return true;
|
|
}
|
|
else if (!strcmp(argv[i], "--help")
|
|
|| !strcmp(argv[i], "-h"))
|
|
{
|
|
opts.display_usage = true;
|
|
return true;
|
|
}
|
|
else if (!strcmp(argv[i], "--debug-info-dir1")
|
|
|| !strcmp(argv[i], "--d1"))
|
|
{
|
|
int j = i + 1;
|
|
if (j >= argc)
|
|
{
|
|
opts.missing_operand = true;
|
|
opts.wrong_option = argv[i];
|
|
return true;
|
|
}
|
|
// elfutils wants the root path to the debug info to be
|
|
// absolute.
|
|
opts.di_root_path1 =
|
|
abigail::tools_utils::make_path_absolute(argv[j]);
|
|
++i;
|
|
}
|
|
else if (!strcmp(argv[i], "--debug-info-dir2")
|
|
|| !strcmp(argv[i], "--d2"))
|
|
{
|
|
int j = i + 1;
|
|
if (j >= argc)
|
|
{
|
|
opts.missing_operand = true;
|
|
opts.wrong_option = argv[i];
|
|
return true;
|
|
}
|
|
// elfutils wants the root path to the debug info to be
|
|
// absolute.
|
|
opts.di_root_path2 =
|
|
abigail::tools_utils::make_path_absolute(argv[j]);
|
|
++i;
|
|
}
|
|
else if (!strcmp(argv[i], "--vmlinux1")
|
|
|| !strcmp(argv[i], "--l1"))
|
|
{
|
|
int j = i + 1;
|
|
if (j >= argc)
|
|
{
|
|
opts.missing_operand = true;
|
|
opts.wrong_option = argv[i];
|
|
return false;
|
|
}
|
|
opts.vmlinux1 = argv[j];
|
|
++i;
|
|
}
|
|
else if (!strcmp(argv[i], "--vmlinux2")
|
|
|| !strcmp(argv[i], "--l2"))
|
|
{
|
|
int j = i + 1;
|
|
if (j >= argc)
|
|
{
|
|
opts.missing_operand = true;
|
|
opts.wrong_option = argv[i];
|
|
return false;
|
|
}
|
|
opts.vmlinux2 = argv[j];
|
|
++i;
|
|
}
|
|
else if (!strcmp(argv[i], "--kmi-whitelist")
|
|
|| !strcmp(argv[i], "-w"))
|
|
{
|
|
int j = i + 1;
|
|
if (j >= argc)
|
|
{
|
|
opts.missing_operand = true;
|
|
opts.wrong_option = argv[i];
|
|
return false;
|
|
}
|
|
opts.kabi_whitelist_paths.push_back(argv[j]);
|
|
++i;
|
|
}
|
|
else if (!strcmp(argv[i], "--suppressions")
|
|
|| !strcmp(argv[i], "--suppr"))
|
|
{
|
|
int j = i + 1;
|
|
if (j >= argc)
|
|
{
|
|
opts.missing_operand = true;
|
|
opts.wrong_option = argv[i];
|
|
return false;
|
|
}
|
|
opts.suppression_paths.push_back(argv[j]);
|
|
++i;
|
|
}
|
|
else if (!strcmp(argv[i], "--impacted-interfaces")
|
|
|| !strcmp(argv[i], "-i"))
|
|
opts.show_impacted_interfaces = true;
|
|
else if (!strcmp(argv[i], "--full-impact")
|
|
|| !strcmp(argv[i], "-f"))
|
|
opts.leaf_changes_only = false;
|
|
else if (!strcmp(argv[i], "--show-bytes"))
|
|
opts.show_offsets_sizes_in_bits = false;
|
|
else if (!strcmp(argv[i], "--show-bits"))
|
|
opts.show_offsets_sizes_in_bits = true;
|
|
else if (!strcmp(argv[i], "--show-hex"))
|
|
opts.show_hexadecimal_values = true;
|
|
else if (!strcmp(argv[i], "--show-dec"))
|
|
opts.show_hexadecimal_values = false;
|
|
else
|
|
{
|
|
opts.wrong_option = argv[i];
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Check that the suppression specification files supplied are
|
|
/// present. If not, emit an error on stderr.
|
|
///
|
|
/// @param opts the options instance to use.
|
|
///
|
|
/// @return true if all suppression specification files are present,
|
|
/// false otherwise.
|
|
static bool
|
|
maybe_check_suppression_files(const options& opts)
|
|
{
|
|
for (vector<string>::const_iterator i = opts.suppression_paths.begin();
|
|
i != opts.suppression_paths.end();
|
|
++i)
|
|
if (!check_file(*i, cerr, "abidiff"))
|
|
return false;
|
|
|
|
for (vector<string>::const_iterator i =
|
|
opts.kabi_whitelist_paths.begin();
|
|
i != opts.kabi_whitelist_paths.end();
|
|
++i)
|
|
if (!check_file(*i, cerr, "abidiff"))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Setup the diff context from the program's options.
|
|
///
|
|
/// @param ctxt the diff context to consider.
|
|
///
|
|
/// @param opts the options to set the context.
|
|
static void
|
|
set_diff_context(diff_context_sptr ctxt, const options& opts)
|
|
{
|
|
ctxt->default_output_stream(&cout);
|
|
ctxt->error_output_stream(&cerr);
|
|
ctxt->show_relative_offset_changes(true);
|
|
ctxt->show_redundant_changes(false);
|
|
ctxt->show_locs(true);
|
|
ctxt->show_linkage_names(false);
|
|
ctxt->show_symbols_unreferenced_by_debug_info
|
|
(true);
|
|
ctxt->show_leaf_changes_only(opts.leaf_changes_only);
|
|
ctxt->show_impacted_interfaces(opts.show_impacted_interfaces);
|
|
ctxt->show_hex_values(opts.show_hexadecimal_values);
|
|
ctxt->show_offsets_sizes_in_bits(opts.show_offsets_sizes_in_bits);
|
|
|
|
ctxt->switch_categories_off(get_default_harmless_categories_bitmap());
|
|
|
|
if (!opts.diff_time_supprs.empty())
|
|
ctxt->add_suppressions(opts.diff_time_supprs);
|
|
}
|
|
|
|
/// Print information about the kernel (and modules) binaries found
|
|
/// under a given directory.
|
|
///
|
|
/// Note that this function actually look for the modules iff the
|
|
/// --verbose option was provided.
|
|
///
|
|
/// @param root the directory to consider.
|
|
///
|
|
/// @param opts the options to use during the search.
|
|
static void
|
|
print_kernel_dist_binary_paths_under(const string& root, const options &opts)
|
|
{
|
|
string vmlinux;
|
|
vector<string> modules;
|
|
|
|
if (opts.verbose)
|
|
if (get_binary_paths_from_kernel_dist(root, /*debug_info_root_path*/"",
|
|
vmlinux, modules))
|
|
{
|
|
cout << "Found kernel binaries under: '" << root << "'\n";
|
|
if (!vmlinux.empty())
|
|
cout << "[linux kernel binary]\n"
|
|
<< " '" << vmlinux << "'\n";
|
|
if (!modules.empty())
|
|
{
|
|
cout << "[linux kernel module binaries]\n";
|
|
for (vector<string>::const_iterator p = modules.begin();
|
|
p != modules.end();
|
|
++p)
|
|
cout << " '" << *p << "' \n";
|
|
}
|
|
cout << "\n";
|
|
}
|
|
}
|
|
|
|
int
|
|
main(int argc, char* argv[])
|
|
{
|
|
options opts;
|
|
if (!parse_command_line(argc, argv, opts))
|
|
{
|
|
emit_prefix(argv[0], cerr)
|
|
<< "unrecognized option: "
|
|
<< opts.wrong_option << "\n"
|
|
<< "try the --help option for more information\n";
|
|
return 1;
|
|
}
|
|
|
|
if (opts.missing_operand)
|
|
{
|
|
emit_prefix(argv[0], cerr)
|
|
<< "missing operand to option: " << opts.wrong_option <<"\n"
|
|
<< "try the --help option for more information\n";
|
|
return 1;
|
|
}
|
|
|
|
if (!maybe_check_suppression_files(opts))
|
|
return 1;
|
|
|
|
if (opts.display_usage)
|
|
{
|
|
display_usage(argv[0], cout);
|
|
return 1;
|
|
}
|
|
|
|
if (opts.display_version)
|
|
{
|
|
emit_prefix(argv[0], cout)
|
|
<< abigail::tools_utils::get_library_version_string()
|
|
<< "\n";
|
|
return 0;
|
|
}
|
|
|
|
environment_sptr env(new environment);
|
|
|
|
corpus_group_sptr group1, group2;
|
|
string debug_info_root_dir;
|
|
if (!opts.kernel_dist_root1.empty())
|
|
{
|
|
file_type ftype = guess_file_type(opts.kernel_dist_root1);
|
|
if (ftype == FILE_TYPE_DIR)
|
|
{
|
|
debug_info_root_dir = opts.di_root_path1.get()
|
|
? opts.di_root_path1.get()
|
|
: "";
|
|
|
|
group1 =
|
|
build_corpus_group_from_kernel_dist_under(opts.kernel_dist_root1,
|
|
debug_info_root_dir,
|
|
opts.vmlinux1,
|
|
opts.suppression_paths,
|
|
opts.kabi_whitelist_paths,
|
|
opts.read_time_supprs,
|
|
opts.verbose,
|
|
env);
|
|
print_kernel_dist_binary_paths_under(opts.kernel_dist_root1, opts);
|
|
}
|
|
else if (ftype == FILE_TYPE_XML_CORPUS_GROUP)
|
|
group1 =
|
|
read_corpus_group_from_native_xml_file(opts.kernel_dist_root1,
|
|
env.get());
|
|
|
|
}
|
|
|
|
if (!opts.kernel_dist_root2.empty())
|
|
{
|
|
file_type ftype = guess_file_type(opts.kernel_dist_root2);
|
|
if (ftype == FILE_TYPE_DIR)
|
|
{
|
|
debug_info_root_dir = opts.di_root_path2.get()
|
|
? opts.di_root_path2.get()
|
|
: "";
|
|
group2 =
|
|
build_corpus_group_from_kernel_dist_under(opts.kernel_dist_root2,
|
|
debug_info_root_dir,
|
|
opts.vmlinux2,
|
|
opts.suppression_paths,
|
|
opts.kabi_whitelist_paths,
|
|
opts.read_time_supprs,
|
|
opts.verbose,
|
|
env);
|
|
print_kernel_dist_binary_paths_under(opts.kernel_dist_root2, opts);
|
|
}
|
|
else if (ftype == FILE_TYPE_XML_CORPUS_GROUP)
|
|
group2 =
|
|
read_corpus_group_from_native_xml_file(opts.kernel_dist_root2,
|
|
env.get());
|
|
}
|
|
|
|
abidiff_status status = abigail::tools_utils::ABIDIFF_OK;
|
|
if (group1 && group2)
|
|
{
|
|
diff_context_sptr diff_ctxt(new diff_context);
|
|
set_diff_context(diff_ctxt, opts);
|
|
|
|
corpus_diff_sptr diff= compute_diff(group1, group2, diff_ctxt);
|
|
|
|
if (diff->has_net_changes())
|
|
status = abigail::tools_utils::ABIDIFF_ABI_CHANGE;
|
|
|
|
if (diff->has_incompatible_changes())
|
|
status |= abigail::tools_utils::ABIDIFF_ABI_INCOMPATIBLE_CHANGE;
|
|
|
|
if (diff->has_changes())
|
|
diff->report(cout);
|
|
}
|
|
else
|
|
status = abigail::tools_utils::ABIDIFF_ERROR;
|
|
|
|
return status;
|
|
}
|