Skip to content

Instantly share code, notes, and snippets.

@scivision
Last active August 11, 2025 03:22
Show Gist options
  • Save scivision/03866efca4ea6a8097f5cfe446c3cabc to your computer and use it in GitHub Desktop.
Save scivision/03866efca4ea6a8097f5cfe446c3cabc to your computer and use it in GitHub Desktop.
C++ can print compiler flags used for the source file

C++ print compiler flags used within the program

The Fortran standard intrinsic compiler_options prints the flags used by the compiler for that binary. Surprisingly, the C++ standard doesn't have such a function. It is possible in GNU GCC-specific fashion to obtain those flags embedded within the binaries at runtime.

cmake --workflow default

Or for CMake < 3.25

cmake -Bbuild
cmake --build build
ctest --test-dir build -V

Supported platforms

GCC and Clang are generally supported on Linux, macOS and Windows.

Reference

  • Simple project to detect CPU feature level including SIMD.
  • MSVC /arch flag
  • MSVC macros that can be used to infer /arch flags used
cmake_minimum_required(VERSION 3.19)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
project(CppCompilerOptions LANGUAGES CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS true)
add_executable(main compiler_options.cpp)
target_compile_options(main PRIVATE $<IF:$<BOOL:${MSVC}>,/W4,-Wall>)
target_compile_features(main PRIVATE cxx_std_17)
if(WIN32)
if(MSVC)
# For MSVC, create a custom section ".cmdline" to store a string.
# This is a placeholder, as MSVC doesn't have a direct equivalent to -frecord-command-line.
# We use the /SECTION linker flag to make the section readable.
target_link_options(main PRIVATE /SECTION:.cmdline,R)
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_options(main PRIVATE "/EHsc")
endif()
endif()
include(GenerateFlags.cmake)
# for GetModuleFileNameExA
target_link_libraries(main PRIVATE Psapi.Lib)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(main PRIVATE -frecord-gcc-switches)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(main PRIVATE -frecord-command-line)
endif()
enable_testing()
add_test(NAME CppCompilerOptions COMMAND main)
file(GENERATE OUTPUT .gitignore CONTENT "*")
/**
* @brief A C++ program to read its own compiler flags.
*
* This program demonstrates how to read compiler options stored in an executable
* by the compiler-specific flags. It requires an ELF-based system like Linux.
* It parses the executable's ELF headers to find the specific section containing the flags.
*
* How it works:
* 1. It locates its own executable file path via the /proc/self/exe symlink.
* 2. It opens the executable file in binary mode.
* 3. It reads the main ELF header to get information about the file structure.
* 4. It locates and reads the Section Header Table.
* 5. It finds the Section Header String Table, which contains the names of all sections.
* 6. It iterates through all sections, looking for one named ".GCC.command.line".
* 7. Once found, it reads the content of that section and prints it. The content
* is a series of null-terminated strings representing the compiler arguments.
*
* References:
*
* https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#index-frecord-gcc-switches
* https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-frecord-command-line
*
* Compilation Instructions:
* You MUST compile this program with the -frecord-* flag appropriate to the compiler
*
* Examples:
* g++ -frecord-gcc-switches -O2 -Wall -o read_flags compiler_options.cpp
*
* clang++ -frecord-command-line -O2 -Wall -o read_flags compiler_options.cpp
*
* Example program output:
*
* GNU C++17 13.3.0 -mtune=generic -march=x86-64 -O2 -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection -fcf-protection
*
* /usr/lib/llvm-18/bin/clang --driver-mode=g++ -frecord-command-line -O2 -Wall -o read_flags compiler_options.cpp -dumpdir read_flags-
*
* Running the program:
* ./read_flags
*/
#include <iostream>
#include <fstream>
#include <string_view>
#include <vector>
#if __has_include(<elf.h>)
#define HAS_ELF
#include <elf.h> // Required for ELF header structures.
#elif defined(__APPLE__)
#include <mach-o/dyld.h> // Required for Mach-O header structures.
#include <mach-o/loader.h>
#include <climits> // for PATH_MAX
#elif defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#else
#error "This is not a supported platform. Try a Linux virtual machine or macOS."
#endif
// A utility function to print the flags from the data buffer.
// The flags are stored as a sequence of null-terminated strings.
void print_flags(const std::vector<char>& buffer) {
const char* current = buffer.data();
const char* end = buffer.data() + buffer.size();
while (current < end) {
std::string_view flag(current);
if (!flag.empty()) {
std::cout << " " << flag << "\n";
}
// Move to the next string
current += flag.length() + 1;
}
}
bool parse_elf(){
bool found = false;
#ifdef HAS_ELF
// On Linux, /proc/self/exe is a symbolic link to the current process's executable.
const char* exe_path = "/proc/self/exe";
std::ifstream file(exe_path, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not open own executable at " << exe_path << "\n";
return false;
}
// 1. Read the main ELF header (we assume 64-bit ELF format)
Elf64_Ehdr elf_header;
file.read(reinterpret_cast<char*>(&elf_header), sizeof(elf_header));
// Basic validation: Check for the ELF magic number
if (file.gcount() != sizeof(elf_header) ||
elf_header.e_ident[EI_MAG0] != ELFMAG0 ||
elf_header.e_ident[EI_MAG1] != ELFMAG1 ||
elf_header.e_ident[EI_MAG2] != ELFMAG2 ||
elf_header.e_ident[EI_MAG3] != ELFMAG3) {
std::cerr << "Error: The file is not a valid ELF executable.\n";
return false;
}
// 2. Read the Section Header Table
file.seekg(elf_header.e_shoff);
std::vector<Elf64_Shdr> section_headers(elf_header.e_shnum);
file.read(reinterpret_cast<char*>(section_headers.data()), elf_header.e_shnum * sizeof(Elf64_Shdr));
if (!file) {
std::cerr << "Error: Could not read section headers.\n";
return false;
}
// 3. Read the Section Header String Table
// This table contains the names of all the sections.
const Elf64_Shdr& shstrtab_header = section_headers[elf_header.e_shstrndx];
std::vector<char> shstrtab(shstrtab_header.sh_size);
file.seekg(shstrtab_header.sh_offset);
file.read(shstrtab.data(), shstrtab_header.sh_size);
if (!file) {
std::cerr << "Error: Could not read section header string table.\n";
return false;
}
// 4. Find the ".GCC.command.line" section and print its contents
for (const auto& section_header : section_headers) {
// sh_name is an offset into the section header string table
std::string_view section_name = &shstrtab[section_header.sh_name];
if (section_name == ".GCC.command.line") {
found = true;
std::cout << "Found compiler flags in section '.GCC.command.line':\n";
// Read the section's content
std::vector<char> flags_buffer(section_header.sh_size);
file.seekg(section_header.sh_offset);
file.read(flags_buffer.data(), section_header.sh_size);
if (file) {
print_flags(flags_buffer);
} else {
std::cerr << "Error: Failed to read the flags data.\n";
}
break; // Stop after finding the section
}
}
#endif
return found;
}
bool parse_macho(){
bool found = false;
#if defined(__APPLE__)
char exe_path[PATH_MAX];
uint32_t size = sizeof(exe_path);
if (_NSGetExecutablePath(exe_path, &size) != 0) {
std::cerr << "Error: Could not get executable path.\n";
return false;
}
std::ifstream file(exe_path, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not open own executable at " << exe_path << "\n";
return false;
}
mach_header_64 header;
file.read(reinterpret_cast<char*>(&header), sizeof(header));
if (header.magic != MH_MAGIC_64) {
std::cerr << "Error: Not a 64-bit Mach-O executable.\n";
return false;
}
// The load commands start right after the header.
// We read them all into a buffer for easier parsing.
std::vector<char> load_commands_buffer(header.sizeofcmds);
file.read(load_commands_buffer.data(), header.sizeofcmds);
if(!file) {
std::cerr << "Error: Could not read load commands.\n";
return false;
}
const char* current_cmd_ptr = load_commands_buffer.data();
for (uint32_t i = 0; i < header.ncmds; ++i) {
const load_command* lc = reinterpret_cast<const load_command*>(current_cmd_ptr);
if (lc->cmd == LC_SEGMENT_64) {
const segment_command_64* seg = reinterpret_cast<const segment_command_64*>(lc);
if (std::string_view(seg->segname) == "__TEXT") {
const section_64* sections = reinterpret_cast<const section_64*>(
reinterpret_cast<const char*>(seg) + sizeof(segment_command_64)
);
for (uint32_t j = 0; j < seg->nsects; ++j) {
const section_64& sect = sections[j];
std::string_view sect_name(sect.sectname);
if (sect_name == "__clang_cmdline" || sect_name == "__command_line") {
found = true;
std::cout << "Found compiler flags in section '__TEXT," << sect_name << "':\n";
std::vector<char> flags_buffer(sect.size);
file.seekg(sect.offset);
file.read(flags_buffer.data(), sect.size);
if (file) {
print_flags(flags_buffer);
} else {
std::cerr << "Error: Failed to read the flags data.\n";
}
return found; // Found it, we are done.
}
}
}
}
current_cmd_ptr += lc->cmdsize; // Move to the next load command
}
#endif
return found;
}
bool parse_pe() {
bool found = false;
#if defined(_WIN32)
char exe_path[MAX_PATH];
if (GetModuleFileNameExA(GetCurrentProcess(), nullptr, exe_path, MAX_PATH) == 0) {
std::cerr << "Error: Could not get executable path. Error code: " << GetLastError() << "\n";
return false;
}
std::ifstream file(exe_path, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not open own executable at " << exe_path << "\n";
return false;
}
IMAGE_DOS_HEADER dos_header;
file.read(reinterpret_cast<char*>(&dos_header), sizeof(dos_header));
if (dos_header.e_magic != IMAGE_DOS_SIGNATURE) {
std::cerr << "Error: Not a valid PE file (Invalid DOS signature).\n";
return false;
}
// Seek to the PE header
file.seekg(dos_header.e_lfanew);
IMAGE_NT_HEADERS64 nt_headers;
file.read(reinterpret_cast<char*>(&nt_headers), sizeof(nt_headers));
if (nt_headers.Signature != IMAGE_NT_SIGNATURE) {
std::cerr << "Error: Not a valid PE file (Invalid NT signature).\n";
return false;
}
// The section headers are right after the NT headers
IMAGE_SECTION_HEADER section_header;
for (int i = 0; i < nt_headers.FileHeader.NumberOfSections; ++i) {
file.read(reinterpret_cast<char*>(&section_header), sizeof(section_header));
std::string_view section_name(reinterpret_cast<const char*>(section_header.Name), 8);
// Trim null characters from the section name
section_name = section_name.substr(0, section_name.find('\0'));
if (section_name == ".cmdline") {
found = true;
std::cout << "Found compiler flags in section '.cmdline':\n";
std::vector<char> flags_buffer(section_header.SizeOfRawData);
file.seekg(section_header.PointerToRawData);
file.read(flags_buffer.data(), static_cast<std::streamsize>(flags_buffer.size()));
if (file) {
print_flags(flags_buffer);
} else {
std::cerr << "Error: Failed to read the flags data.\n";
}
break;
}
}
#endif
return found;
}
int main() {
bool found = false;
#if defined HAS_ELF
found = parse_elf();
#elif defined(__APPLE__)
found = parse_macho();
#elif defined(_WIN32)
found = parse_pe();
#endif
if (!found) {
std::cerr << "Could not find the compiler options section; this compiler/platform may not be supported.\n";
return 1;
}
return 0;
}
function(detect_mingw_flags cmake_flags cflags outvar)
# NVHPC works the same way, but the default "-tp host" is like "-mtune=native".
# the command would be like "nvc++ ${cmake_flags} ${cflags} -E -v /dev/null"
# then filter with regex after compilers/bin/tools/nvcpfe --
if(NOT DEFINED raw_options)
execute_process(COMMAND ${CMAKE_CXX_COMPILER} ${cmake_flags} ${cflags} -E -v -xc++ NUL
OUTPUT_VARIABLE cmd_out
ERROR_VARIABLE cmd_err
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE ret
COMMAND_ECHO STDOUT
)
if(NOT ret EQUAL 0)
message(WARNING "Failed to execute compiler command.
${ret}
${cmd_err}")
endif()
message(DEBUG "cmd_out: ${cmd_out}")
message(DEBUG "cmd_err: ${cmd_err}")
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# set(pat "COLLECT_GCC_OPTIONS=([^\r\n]*)")
set(pat "cc1plus.exe ([^\r\n]*) NUL")
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# The line with the command starts with the compiler path, often in quotes.
set(pat "^[ \t]*\"?.*clang.*\\.exe\"? ([^\r\n]*) -o - -x c\\+\\+ NUL")
endif()
if(cmd_err MATCHES ${pat})
set(raw_options "${CMAKE_MATCH_1}")
else()
message(WARNING "Could not find compiler flags in compiler output")
set(raw_options "")
endif()
set(raw_options "${raw_options}" CACHE INTERNAL "")
endif()
# Remove flags that are for pre-processing, verbosity, or system include dirs
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(pat "(-E |-v |-quiet|-dumpbase|NUL|-iprefix[ ]+(\"[^\"]+\"|[^ ]+))")
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(pat "(-v |-(internal-isystem|resource-dir)[ ]+(\"[^\"]+\"|[^ ]+)|-f(debug|coverage)-compilation-dir=[^ ]+)")
endif()
string(REGEX REPLACE ${pat} "" cleaned_options "${raw_options}")
# Escape for C++ string literal
string(REPLACE "\\" "\\\\" escaped "${cleaned_options}")
string(REPLACE "\"" "\\\"" escaped "${escaped}")
string(STRIP "${escaped}" escaped)
set(${outvar} "${escaped}" PARENT_SCOPE)
endfunction()
get_target_property(cflags main COMPILE_OPTIONS)
get_target_property(cdefs main COMPILE_DEFINITIONS)
if(NOT cflags)
set(cflags "")
endif()
if(NOT cdefs)
set(cdefs "")
endif()
string(REPLACE ";" " " cflags "${cflags}" "${cdefs}")
set(cmake_flags ${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}})
# Generate source attribute file
if(MSVC)
set(generated_source_content
"__pragma(section(\".cmdline\", read))\n"
"extern \"C\" {\n"
"__declspec(allocate(\".cmdline\")) extern const char g_command_line[] = \"cl.exe ${cmake_flags} ${cflags}\";\n"
"}\n"
"__pragma(comment(linker, \"/include:g_command_line\"))\n"
)
else()
detect_mingw_flags("${cmake_flags}" "${cflags}" escaped)
set(generated_source_content
"const char g_compiler_flags[] __attribute__((section(\".cmdline\"), used)) = \"${escaped}\";"
)
endif()
file(WRITE "${CMAKE_BINARY_DIR}/generated_flags.cpp" "${generated_source_content}")
target_sources(main PRIVATE "${CMAKE_BINARY_DIR}/generated_flags.cpp")
program test
use, intrinsic :: iso_fortran_env
print '(a)', compiler_options()
end program
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment