Skip to content

A Checked Array Project

Combine the previous lessons into a signed-array report with an explicit result pointer. Test the empty case, mixed signs and wider accumulation against an independent C implementation.

Before you start

Complete GDB debugging. You can trace a load, prove a finite loop and describe a function boundary.

Outcome: implement a sum-and-negative-count operation, check caller-side validation and explain its bounds. Use a fresh directory on the Pi. This project uses fixture arrays, not sensor data.

Agree on the public contract

The assembly function accepts a readable array of n signed 32-bit values and a writable eight-byte count pointer that does not overlap the input. For n=0, the input may be null, but the count pointer must still be valid. It returns the signed 64-bit sum and stores the number of negative elements.

The C wrapper accepts n <= 1024 and rejects null pointers for nonempty inputs. It cannot prove that an arbitrary non-null pointer is valid; storage extent and lifetime remain caller obligations. The bound keeps every allowed mathematical sum within int64_t.

Implement the core

// file: report.S
.text
.global summarize_i32
.type summarize_i32, %function
summarize_i32:
    mov x3, #0                 // sum
    mov x4, #0                 // negative count
    cbz x1, .Ldone
.Lloop:
    ldrsw x5, [x0], #4
    add x3, x3, x5
    cmp x5, #0
    cset x6, lt
    add x4, x4, x6
    subs x1, x1, #1
    b.ne .Lloop
.Ldone:
    str x4, [x2]
    mov x0, x3
    ret
.size summarize_i32, .-summarize_i32
.section .note.GNU-stack,"",%progbits

Notice the order of flag uses: CSET consumes the comparison with zero; B.NE consumes the subsequent decrement. Interchanging them would test a different fact.

Test with an independent oracle

/* file: report.c */
#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
extern int64_t summarize_i32(const int32_t *, uint64_t, uint64_t *);
static int checked_summary(const int32_t *p, size_t n,
                           int64_t *sum, uint64_t *negative)
{
    if (!sum || !negative || n > 1024 || (n && !p)) return 0;
    *sum = summarize_i32(p, n, negative);
    return 1;
}
static int check(const int32_t *p, size_t n)
{
    int64_t expected_sum = 0, actual_sum = 0;
    uint64_t expected_negative = 0, actual_negative = UINT64_MAX;
    for (size_t i = 0; i < n; ++i) {
        expected_sum += p[i];
        expected_negative += p[i] < 0;
    }
    return checked_summary(p, n, &actual_sum, &actual_negative) &&
           actual_sum == expected_sum && actual_negative == expected_negative;
}
int main(void)
{
    const int32_t mixed[] = {-5, 0, 9, -2};
    const int32_t extremes[] = {INT32_MIN, INT32_MAX, INT32_MAX};
    const int32_t positive[] = {4, 5, 6};
    int32_t full[1024];
    for (unsigned i = 0; i < 1024; ++i) full[i] = (int32_t)i - 512;
    int64_t sum = 0;
    uint64_t negative = 0;
    if (!check(NULL, 0) || !check(mixed, 1) || !check(mixed, 4) ||
        !check(extremes, 3) || !check(positive, 3) || !check(full, 1024) ||
        checked_summary(NULL, 1, &sum, &negative) ||
        checked_summary(mixed, 1025, &sum, &negative) ||
        checked_summary(mixed, 4, NULL, &negative) ||
        checked_summary(mixed, 4, &sum, NULL)) return 1;
    if (!checked_summary(mixed, 4, &sum, &negative)) return 1;
    printf("sum: %" PRId64 "\nnegative: %" PRIu64 "\n", sum, negative);
    puts("PASS report: oracle, boundaries and rejected inputs");
    return 0;
}
1
2
3
4
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -g -O0 report.c report.S -o report
./report
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -g -O2 report.c report.S -o report-O2
./report-O2

Each run should print:

1
2
3
sum: 2
negative: 2
PASS report: oracle, boundaries and rejected inputs

The oracle uses a different expression of the algorithm, but passing these fixtures is not proof for every accessible array. Compiler sanitizers do not automatically instrument memory accesses inside hand-written assembly.

Part 3 checkpoint

Explain why the empty call still writes a count; why a null output must be rejected; why INT32_MIN must be sign-extended; and why an int32_t sum would be too narrow.

Add an all-negative fixture and a case with a sum larger than INT32_MAX. Keep the oracle independent. If a test fails, use the GDB loop invariant rather than changing the expected answer.

Extend by a requirement, not an instruction count

A minimum/maximum report needs an explicit empty-result policy. A checked-overflow variant needs a different contract. A hardware input adapter needs separate I/O error handling. None follows automatically from adding more assembly.

Check your understanding

Can checked_summary validate a dangling non-null pointer? What is the loop invariant?

Compare your reasoning

No; the caller must supply live storage. Before each iteration the sum/count summarize exactly the consumed prefix and the pointer/count describe the unread suffix.

Continue the course

Previous: Debugging with GDB · Course roadmap · Next: NEON and Scalar Tails

Donate