Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
*.exe
*.dll
*.map
*.gcda
*.gcno
*~
.*.swp
.DS_Store
.vscode/
doc/reference/
environ.sh
romdisk.img
coverage.info
kernel/stubs/*.c
kernel/exports/kernel_exports.c
kernel/arch/dreamcast/kernel/arch_exports.c
Expand Down
152 changes: 152 additions & 0 deletions addons/include/gcov/gcov.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* KallistiOS ##version##

gcov/gcov.h
Copyright (C) 2025 Andy Barajas
*/

/** \file gcov/gcov.h
\brief Minimal GCOV runtime implementation.
\ingroup gcov

This file defines the public interface for the GCOV profiling runtime.
It enables GCC's `--coverage` and `-fprofile-generate` functionality. The
implementation is compatible with GCC 13+ and supports generation and manual
dumping of `.gcda` coverage files, including full support for all standard
counter types.

\author Andy Barajas
*/

#ifndef __GCOV_GCOV_H
#define __GCOV_GCOV_H

#include <sys/cdefs.h>
__BEGIN_DECLS

/** \defgroup gcov GCOV
\brief Lightweight GCOV profiling runtime for KOS
\ingroup debugging

This file provides runtime support for GCOV, a coverage analysis tool built
into GCC. GCOV allows you to determine which parts of your code were executed,
how often, and which branches were taken. This is especially helpful for
analyzing test coverage or identifying unused code paths.

Supported Profiling Modes:

1. `-fprofile-generate`:

Enables profile data generation. This inserts instrumentation into your
code to collect execution counts for functions, branches, and arcs during
runtime. The results are written to `.gcda` files when `__gcov_exit()` or
`__gcov_dump()` is called.

Example:
```sh
CFLAGS += -fprofile-generate
```

2. `-fprofile-use`:

Recompiles your code using previously collected `.gcda` files (from
`-fprofile-generate`) to guide optimizations. This can improve performance
by reordering code based on actual runtime behavior.

Example:
```sh
CFLAGS += -fprofile-use
```

3. `-ftest-coverage` (or `--coverage`):

Enables both `-fprofile-arcs` and `-ftest-coverage`, which insert
instrumentation and generate `.gcno` metadata files during compilation.
These `.gcno` files are required to interpret `.gcda` data later.

Example:
```sh
CFLAGS += --coverage
```

Collecting and Analyzing Coverage:

1. **Compile with coverage support:**

```sh
CFLAGS += --coverage
```

This generates `.gcno` files alongside your object files.

2. **Run your program on Dreamcast (with `-fprofile-generate`):**

Coverage data will be collected in memory during execution. You can manually
trigger a dump at any point by calling:

```c
__gcov_dump(); // or __gcov_exit();
```

This writes `.gcda` files to the filesystem, redirected to `/pc` by default.

3. **Generate an HTML report with LCOV:**

Use this Makefile target to capture and visualize results:

```make
lcov:
lcov \
--gcov-tool=/opt/toolchains/dc/sh-elf/bin/sh-elf-gcov \
--directory . \
--base-directory . \
--capture \
--output-file coverage.info
genhtml coverage.info --output-directory html
open html/index.html
```

This generates a full HTML report with annotated source code in the `html/` directory.

\author Andy Barajas

@{
*/

/** \brief Environment variable to set the output directory for `.gcda` files.
\ingroup gcov

If set, this value is prepended to the stripped source path to form the
final output path.
*/
#define GCOV_PREFIX "GCOV_PREFIX"

/** \brief Environment variable to control path stripping.
\ingroup gcov

Specifies how many leading directory components should be removed from the
source file path before generating the `.gcda` output file.
*/
#define GCOV_PREFIX_STRIP "GCOV_PREFIX_STRIP"

/** \brief Clears all collected runtime coverage counters.
\ingroup gcov

This function resets all counters in memory without writing them out.
Useful for restarting coverage collection mid-run.
*/
void __gcov_reset(void);

/** \brief Writes all registered coverage data to `.gcda` files.
\ingroup gcov

This function flushes all registered coverage counters to disk using the
current GCOV output rules. Called automatically on exit, but can also be
called manually for intermediate coverage snapshots.
*/
void __gcov_dump(void);

/** @} */

__END_DECLS

#endif /* !__GCOV_GCOV_H */
9 changes: 9 additions & 0 deletions addons/libgcov/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# libgcov Makefile
#
# Copyright (C) 2025 Andy Barajas
#

TARGET = libgcov.a
OBJS = gcov.o profiler.o merge.o dump.o reset.o topn.o filepath.o

include $(KOS_BASE)/addons/Makefile.prefab
191 changes: 191 additions & 0 deletions addons/libgcov/dump.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/* KallistiOS ##version##

dump.c
Copyright (C) 2025 Andy Barajas

*/

#include <kos/dbglog.h>

#include "gcov.h"

#define GCOV_TAG_EOF 0

#define GCOV_MAGIC ((uint32_t)0x67636461) /* "gcda" */
#define GCOV_TAG_FUNCTION ((uint32_t)0x01000000)
#define GCOV_TAG_COUNTER_BASE 0x01a10000
#define GCOV_TAG_FOR_COUNTER(CNT) (GCOV_TAG_COUNTER_BASE + ((uint32_t)(CNT) << 17))
#define GCOV_COUNTER_FOR_TAG(TAG) (((TAG) - GCOV_TAG_COUNTER_BASE) >> 17)

/* So merge function pointers can have access to data we want to merge */
gcov_type *merge_temp_buf = NULL;

static inline bool are_all_counters_zero(const gcov_ctr_info_t *ci_ptr) {
for(uint32_t i = 0; i < ci_ptr->num; i++) {
if(ci_ptr->values[i] != 0)
return false;
}

return true;
}

/* Reads a .gcda file and merges its counters into the current gcov_info_t */
static void merge_existing_gcda(const gcov_info_t *info) {
FILE *f;
int32_t length;
uint32_t tag;
uint32_t fn_idx = 0;
uint32_t ctr_idx = 0;
char out_path[GCOV_FILEPATH_MAX];

gcov_build_filepath(info->filepath, out_path);

f = fopen(out_path, "rb");
if(!f)
return;

/* Check MAGIC */
if(fread(&tag, 4, 1, f) != 1 || tag != GCOV_MAGIC)
goto done;

/* Skip version, stamp, and checksum */
fseek(f, 12, SEEK_CUR);

/* Read TAG blocks */
while(fread(&tag, 4, 1, f) == 1) {
if(tag == GCOV_TAG_EOF)
break;

/* Read TAG length (in bytes) */
if(fread(&length, 4, 1, f) != 1)
break;

if(tag == GCOV_TAG_FUNCTION) {
/* Skip ident, lineno_checksum, and cfg_checksum */
fseek(f, length, SEEK_CUR);
fn_idx++;
/* Reset counters index for new function */
ctr_idx = 0;
}
else if(tag >= GCOV_TAG_FOR_COUNTER(0) &&
tag < GCOV_TAG_FOR_COUNTER(GCOV_COUNTERS)) {

/* Skip if negative - Counters are all zero */
if(length < 0) {
ctr_idx++;
continue;
}

int mrg_idx = GCOV_COUNTER_FOR_TAG(tag);
const gcov_fn_info_t *fn = info->functions[fn_idx - 1];

if(fn && info->merge[mrg_idx]) {
gcov_type *buffer = NULL;
if(posix_memalign((void **)&buffer, 8, length))
break;

if(fread(buffer, length, 1, f) != 1) {
free(buffer);
break;
}

uint32_t num_counters = length / sizeof(gcov_type);
merge_temp_buf = buffer;
info->merge[mrg_idx](fn->ctrs[ctr_idx].values, num_counters);
merge_temp_buf = NULL;
free(buffer);
}
else {
/* This shouldnt happen */
fseek(f, length, SEEK_CUR);
}

ctr_idx++;
}
else {
/* Skip unknown tag block */
fseek(f, length, SEEK_CUR);
}
}
dbglog(DBG_ERROR, "GCOV: Done merging file%s\n", out_path);
done:
fclose(f);
}

static void write_gdca(const gcov_info_t *info) {
FILE *f;
bool all_zero;
uint32_t i, j, k, tmp, length;
char out_path[GCOV_FILEPATH_MAX];

gcov_build_filepath(info->filepath, out_path);

f = fopen(out_path, "wb");
if(!f) {
dbglog(DBG_ERROR, "GCOV: Failed to open %s\n", out_path);
return;
}

/* File header */
tmp = GCOV_MAGIC;
fwrite(&tmp, 1, 4, f);
fwrite(&info->version, 1, 4, f);
fwrite(&info->stamp, 1, 4, f);
fwrite(&info->checksum, 1, 4, f);

/* Each function */
for(i = 0; i < info->num_functions; ++i) {
const gcov_fn_info_t *fn = info->functions[i];
length = (fn && fn->key == info) ? 12 : 0;

/* Tag: Function header */
tmp = GCOV_TAG_FUNCTION;
fwrite(&tmp, 1, 4, f);
fwrite(&length, 1, 4, f); /* # of following bytes */
if(!length) continue;

fwrite(&fn->ident, 1, 4, f);
fwrite(&fn->lineno_checksum, 1, 4, f);
fwrite(&fn->cfg_checksum, 1, 4, f);

/* Each Counter type */
const gcov_ctr_info_t *ci = fn->ctrs;
for(j = 0; j < GCOV_COUNTERS; ++j) {
if(!info->merge[j])
continue;

tmp = GCOV_TAG_FOR_COUNTER(j);
fwrite(&tmp, 1, 4, f);

if(j == GCOV_COUNTER_V_TOPN || j == GCOV_COUNTER_V_INDIR)
topn_write(f, ci);
else {
all_zero = are_all_counters_zero(ci);

tmp = sizeof(gcov_type) * (all_zero ? -ci->num : ci->num);
fwrite(&tmp, 1, 4, f);

if(!all_zero) {
for(k = 0; k < ci->num; ++k) {
fwrite(&ci->values[k], 1, sizeof(gcov_type), f);
}
}
}

ci++;
}
}

/* Terminator */
tmp = GCOV_TAG_EOF;
fwrite(&tmp, 1, 4, f);

fclose(f);

dbglog(DBG_NOTICE, "GCOV: dumped %s\n", out_path);
}

void dump_info(const gcov_info_t *info) {
merge_existing_gcda(info);
write_gdca(info);
}
Loading