Google Embedded Developer (Staff Level) Interview Preparation Guide
Google's Embedded Developer interview process for Staff level typically consists of an initial recruiter screening, technical phone screen(s) focusing on embedded systems fundamentals and coding, followed by 5-7 onsite rounds including embedded systems design, low-level programming assessments, system architecture discussions, and behavioral/culture fit evaluations. The process emphasizes practical embedded knowledge, C/C++ proficiency, hardware-software integration understanding, and demonstrated experience with real-world embedded systems and driver development.
Interview Rounds
Recruiter Screening
What to Expect
Initial conversation with Google recruiter to verify background, confirm role fit, discuss experience with embedded systems and hardware platforms, and explain motivation for Staff-level position. Follow-up may include discussion of compensation and role expectations.
Tips & Advice
Clearly articulate your embedded systems background and progression to Staff level. Highlight leadership experiences mentoring engineers, cross-team collaboration, and ownership of complex embedded projects. Be specific about hardware platforms and architectures you've worked with (microcontrollers, SoCs, IoT devices). Discuss why you're seeking this role at Google specifically. Show enthusiasm for the intersection of hardware and software. Ask informed questions about the team structure and impact areas.
Focus Topics
Motivation for Google and Role Fit
Clear explanation of why you're interested in Google's embedded systems work and how this role aligns with your career goals
Practice Interview
Study Questions
Leadership and Mentorship Examples
Specific examples of how you've led embedded teams, mentored junior engineers, influenced architectural decisions, and driven cross-functional collaboration
Practice Interview
Study Questions
Embedded Systems Experience Overview
Concise summary of your career progression in embedded development, platforms worked with (ARM, RISC-V, x86, custom SoCs), and scale of systems you've managed
Practice Interview
Study Questions
Technical Phone Screen - Embedded Systems Fundamentals
What to Expect
Phone-based technical assessment covering core embedded systems concepts, C programming proficiency, and real-time system knowledge. Interviewer will explore your understanding of interrupt handling, memory management, and low-level hardware interaction. May include brief coding problems involving bit manipulation, arrays, and embedded-specific algorithms.
Tips & Advice
Focus on demonstrating deep knowledge of embedded fundamentals rather than algorithmic complexity. Be prepared to discuss interrupt handlers, memory constraints, and real-time requirements. C programming should be fluent—be able to explain data types, memory layouts, and pointer arithmetic. Discuss practical trade-offs (speed vs. memory, power vs. performance). Use a collaborative approach, explaining your thought process clearly. If you mention specific embedded projects on your resume, be ready to deep-dive into technical details and driver implementations.
Focus Topics
IP and Driver Implementation Knowledge
Understanding of any specific IPs (Intellectual Properties) mentioned in your resume, driver architecture, and device driver development patterns
Practice Interview
Study Questions
Bit Manipulation and Bitwise Operations
Mastery of bitwise AND, OR, XOR, shift operations, bit masks, and bit field operations for register manipulation and efficient data storage
Practice Interview
Study Questions
Memory Optimization and Constraints
Strategies for optimizing memory usage in resource-constrained environments, understanding memory hierarchies, cache behavior, and managing limited RAM/ROM
Practice Interview
Study Questions
Hardware Abstraction and Register Access
Knowledge of memory-mapped I/O, volatile keyword usage, register definitions, hardware data sheets, and safe methods for controlling hardware peripherals
Practice Interview
Study Questions
Interrupt Handling and Real-Time Concepts
Understanding of interrupt service routines (ISRs), context switching, priority levels, interrupt masking, and real-time operating system (RTOS) concepts
Practice Interview
Study Questions
C Programming Fundamentals for Embedded Systems
Deep knowledge of C data types, memory management (stack vs. heap), pointer manipulation, bit operations, and struct/union usage specific to embedded contexts
Practice Interview
Study Questions
Technical Phone Screen - Embedded Design and Coding
What to Expect
Second technical phone screen focusing on practical embedded systems design, problem-solving under constraints, and coding implementations. This round typically features problems that require both algorithmic thinking and embedded systems knowledge—such as implementing bit-level operations for hardware control, designing memory-efficient data structures, or solving real-time scheduling problems.
Tips & Advice
Approach problems by discussing constraints upfront (memory limits, power budget, latency requirements). Show how you'd optimize for embedded environments. Write clean, efficient C code with proper error handling. Discuss testing strategies in resource-constrained environments. If given a problem, clarify requirements before coding. Explain trade-offs clearly (e.g., using lookup tables vs. computation). For Staff level, interviewers expect you to consider system-wide implications, not just local optimization.
Focus Topics
Ring Buffers and Circular Data Structures
Implementation and use of ring buffers, circular queues, and efficient data structures for embedded systems with memory constraints
Practice Interview
Study Questions
Timing, Synchronization, and Concurrency
Understanding of timing requirements, synchronization primitives (mutexes, semaphores), race conditions, and concurrent execution in embedded contexts
Practice Interview
Study Questions
State Machines and Protocol Implementation
Design and implementation of finite state machines for device drivers and communication protocols, managing state transitions and edge cases
Practice Interview
Study Questions
Low-Level Code Implementation
Writing efficient C code for embedded contexts including handling edge cases, avoiding common pitfalls (integer overflow, unaligned access), and optimizing for embedded compilers
Practice Interview
Study Questions
Embedded Problem-Solving Under Constraints
Approach to solving design problems with explicit resource constraints: limited memory, processing power, power budget, real-time deadlines
Practice Interview
Study Questions
Onsite Round 1 - Embedded Systems Architecture Deep Dive
What to Expect
In-person interview examining your understanding of complex embedded system architectures. You'll be asked to discuss real systems you've worked with, analyze architectural decisions, discuss trade-offs (power, performance, reliability), and potentially design embedded system components from first principles. Interviewer probes your ability to think systemically about hardware-software co-design.
Tips & Advice
Prepare 2-3 detailed case studies of complex embedded projects you've led. Walk through your architectural decisions: why you chose certain processors, communication protocols, memory hierarchies. Discuss failures and how you recovered. Be ready to sketch block diagrams and explain signal flow. For Staff level, interviewers want to hear about scalability of your designs and how you've applied lessons across multiple products. Discuss how you'd approach designing similar systems differently given new constraints. Show systems thinking beyond code.
Focus Topics
Performance Analysis and Optimization
Methods for profiling embedded systems, identifying bottlenecks, optimizing for latency and throughput, and balancing competing performance metrics
Practice Interview
Study Questions
Power Management and Battery Optimization
Techniques for power consumption analysis, low-power modes, sleep states, dynamic voltage and frequency scaling (DVFS), and battery life optimization
Practice Interview
Study Questions
Communication Protocols and Interfaces
Deep understanding of common embedded protocols (I2C, SPI, UART, CAN, USB) and their use cases, advantages, and limitations
Practice Interview
Study Questions
Case Study: Leadership of Complex Embedded Project
Detailed discussion of a complex embedded system you led: architecture, team composition, technical challenges, decision-making process, and lessons learned
Practice Interview
Study Questions
Embedded System Architecture Patterns
Understanding of common embedded architectures (microkernel, monolithic, layered), their trade-offs, and appropriate contexts for each
Practice Interview
Study Questions
Hardware-Software Co-Design and Integration
Knowledge of how hardware capabilities influence software design, peripheral integration, SoC selection, and optimizing for specific hardware platforms
Practice Interview
Study Questions
Onsite Round 2 - Device Driver and Firmware Development
What to Expect
Technical interview focused on your expertise in device driver development and firmware implementation. You'll discuss driver architecture, interrupt handling in driver context, DMA operations, memory mapping, and handling hardware-specific issues. May include coding or whiteboard design of driver components. Interviewer assesses your practical experience with real hardware and ability to debug complex hardware-software interaction issues.
Tips & Advice
This is where your practical experience shines. Be specific about drivers you've written: what hardware, what challenges, how you debugged. Discuss interrupt handlers, concurrency in drivers, and synchronization. Be ready to explain common driver patterns and pitfalls (blocking operations, timeout handling, resource cleanup). If you've worked with kernel drivers or bootloaders, highlight that expertise. Discuss how you've diagnosed and fixed hardware-software timing issues. For Staff level, show how you've mentored others in driver development.
Focus Topics
Bootloader and Firmware Update Mechanisms
Understanding of bootloader design, firmware loading, in-place updates, rollback mechanisms, and recovery procedures
Practice Interview
Study Questions
Debugging Hardware-Software Integration Issues
Techniques for diagnosing timing issues, race conditions, hardware/firmware compatibility problems using logic analyzers, debuggers, and instrumentation
Practice Interview
Study Questions
Direct Memory Access (DMA) and Memory Mapping
Understanding of DMA operations, memory alignment, scatter-gather lists, and memory mapping techniques for efficient data transfer in drivers
Practice Interview
Study Questions
Interrupt Handlers and ISR Context Programming
Proper implementation of interrupt service routines, minimizing ISR complexity, deferred work (bottom-half handlers), and avoiding ISR-safe violations
Practice Interview
Study Questions
Device Driver Architecture and Design
Understanding of driver layers, abstraction models, platform drivers vs. device drivers, and design patterns for scalable driver implementations
Practice Interview
Study Questions
Onsite Round 3 - Real-Time Systems and Operating Systems
What to Expect
Interview covering real-time operating system (RTOS) concepts, real-time scheduling, timing guarantees, and task management. Discussion may include your experience with RTOS platforms (FreeRTOS, QNX, VxWorks), handling priority inversion, deterministic behavior, and designing systems with strict timing requirements. Interviewer evaluates your understanding of RTOS concepts and their practical application.
Tips & Advice
Discuss specific RTOS experience you have. Explain concepts like context switching, preemption, priority levels, and scheduling algorithms. Discuss real-world timing issues you've solved. Be ready to analyze scenarios (e.g., 'what happens if a high-priority task becomes blocked?'). Explain techniques for achieving determinism and avoiding timing surprises. For Staff level, discuss how you've designed systems to meet strict timing requirements and how you've improved timing predictability.
Focus Topics
Memory-Constrained Real-Time Systems
Designing RTOS-based systems with limited memory, stack management, heap fragmentation prevention, and memory safety in real-time contexts
Practice Interview
Study Questions
Timing Analysis and Worst-Case Execution Time (WCET)
Methods for analyzing timing behavior, calculating worst-case execution times, identifying timing bottlenecks, and proving timing guarantees
Practice Interview
Study Questions
Synchronization Primitives and Priority Inversion
Proper use of mutexes, semaphores, condition variables, priority ceiling protocols, and avoiding/mitigating priority inversion problems
Practice Interview
Study Questions
Real-Time Scheduling and Task Management
Understanding of scheduling algorithms (rate monotonic, EDF), task prioritization, deadline management, and handling overload conditions
Practice Interview
Study Questions
Real-Time Operating System (RTOS) Fundamentals
Core RTOS concepts including task scheduling, context switching, priority levels, preemption, and deterministic behavior guarantees
Practice Interview
Study Questions
Onsite Round 4 - Low-Level Programming and Optimization
What to Expect
Technical interview involving low-level C/C++ programming, inline assembly, compiler optimizations, and performance tuning. May include analyzing assembly output, understanding compiler behavior, optimizing tight loops, and reducing code size for embedded systems. Interviewer assesses your understanding of how high-level code maps to hardware execution.
Tips & Advice
Demonstrate comfort reading and understanding assembly language. Discuss compiler pragmas, inline assembly usage, and volatile keyword. Show understanding of calling conventions and stack layouts. Be ready to optimize code for size or speed trade-offs. Discuss benchmarking and profiling techniques. For Staff level, show how you've approached system-wide optimization rather than micro-optimizations. Discuss when to optimize and when not to. Show understanding of embedded toolchains.
Focus Topics
Embedded Toolchains and Build Systems
Understanding of cross-compilers, linker scripts, embedded build tools, and debugging with embedded debuggers and JTAG interfaces
Practice Interview
Study Questions
Code Size Optimization Techniques
Strategies for reducing code size in ROM-constrained systems: dead code elimination, compression, function inlining decisions, and code reuse patterns
Practice Interview
Study Questions
Performance Profiling and Benchmarking
Techniques for measuring execution time, memory usage, power consumption, and identifying optimization opportunities in embedded code
Practice Interview
Study Questions
Compiler Optimization and Pragmas
Understanding of compiler optimization levels, inline functions, pragmas for performance/size tuning, and compiler-specific extensions
Practice Interview
Study Questions
Assembly Language and CPU Architecture
Understanding of assembly language, instruction sets (ARM, RISC-V, x86), calling conventions, and how C code maps to hardware execution
Practice Interview
Study Questions
Onsite Round 5 - Behavioral, Leadership, and Culture Fit
What to Expect
Behavioral interview assessing communication skills, teamwork, handling conflict, and Google cultural fit. Interviewer explores your leadership experiences mentoring team members, managing projects, handling technical disagreements, and driving decisions. This round evaluates how you collaborate across teams, advocate for your ideas, and contribute to team growth. Emphasis on specific examples demonstrating Staff-level impact and influence.
Tips & Advice
Prepare detailed STAR (Situation, Task, Action, Result) examples of: mentoring junior engineers in embedded systems, leading a complex embedded project with cross-functional teams, handling disagreement about technical approach, and improving team processes. Emphasize impact on team and organization. Show how you've grown as a leader and how you help others grow. Discuss failure and learning. Ask thoughtful questions about team structure, culture, and impact. Demonstrate genuine interest in Google's embedded systems work.
Focus Topics
Google Culture and Values Alignment
Understanding of Google's approach to technology, innovation, quality, and how your values and work style align with the company
Practice Interview
Study Questions
Communication and Technical Writing
Ability to explain complex embedded concepts to diverse audiences, document designs, and communicate with non-technical stakeholders
Practice Interview
Study Questions
Cross-Functional Collaboration
Examples of working with hardware engineers, firmware teams, and other disciplines to deliver complete embedded solutions
Practice Interview
Study Questions
Handling Failure and Learning from Mistakes
Examples of significant technical failures, how you diagnosed root causes, what you learned, and how you prevented recurrence
Practice Interview
Study Questions
Technical Leadership and Decision-Making
Examples of leading embedded projects, making architectural decisions, advocating for technical choices, and handling disagreement with data and reasoning
Practice Interview
Study Questions
Mentorship and Team Development
Specific examples of mentoring junior engineers in embedded systems, helping them grow technically, and contributing to team capability building
Practice Interview
Study Questions
Frequently Asked Embedded Developer Interview Questions
Given two large arrays A and B of uint64_t fingerprints (length N up to 1e6), implement an efficient routine in C that computes the Hamming distance for each pair: out[i] = popcountll(A[i] ^ B[i]) and stores results in an 8-bit array. Discuss optimizations for memory bandwidth, usage of hardware POPCNT, SIMD, multithreading, and embedded SoC constraints (cache, DMA).
Sample Answer
Approach (brief)
Compute out[i] = popcountll(A[i] ^ B[i]) in a bandwidth- and CPU-efficient loop. Use 64-bit XOR + hardware POPCNT where available, fallback to compiler builtin; apply SIMD/NEON for throughput, blocking to fit caches, and threads or DMA for large transfers on SoC.
Example C implementation (single-threaded, portable with POPCNT):
#include <stdint.h>
#include <stddef.h>
#include <x86intrin.h> // for _mm_popcnt_u64 on x86
void hamming_u64(const uint64_t *A, const uint64_t *B, uint8_t *out, size_t N) {
size_t i = 0;
// Process 4-at-a-time to help ILP and prefetching
for (; i + 3 < N; i += 4) {
uint64_t x0 = A[i+0] ^ B[i+0];
uint64_t x1 = A[i+1] ^ B[i+1];
uint64_t x2 = A[i+2] ^ B[i+2];
uint64_t x3 = A[i+3] ^ B[i+3];
out[i+0] = (uint8_t)_mm_popcnt_u64(x0);
out[i+1] = (uint8_t)_mm_popcnt_u64(x1);
out[i+2] = (uint8_t)_mm_popcnt_u64(x2);
out[i+3] = (uint8_t)_mm_popcnt_u64(x3);
}
for (; i < N; ++i)
out[i] = (uint8_t)_mm_popcnt_u64(A[i] ^ B[i]);
}
Optimizations & reasoning
- Hardware POPCNT / intrinsic (_mm_popcnt_u64 or __builtin_popcountll) gives minimal cycles per 64-bit popcount.
- Loop unrolling (4-at-a-time) increases ILP and amortizes memory latency.
- Prefetch/align arrays to 64B to improve cache line utilization; process in blocks sized to L1/L2 cache (e.g., 32KB blocks) to avoid thrashing.
- SIMD: on ARM NEON, load vectors and use vector table or pairwise popcount emulation (NEON lacks scalar 64-bit popcnt on older cores) — newer ARMv8.2 has vcnt for bytes; XOR then vcnt+horizontal sum to get counts for 8 lanes.
- Multithreading: split into contiguous chunks per core to maximize cache locality; avoid false sharing by aligning output chunk boundaries to cache lines.
- Embedded SoC constraints: if DMA available, use DMA to stage A/B into tightly aligned SRAM, then compute in place to reduce external memory bandwidth. For tiny RAM, process in smaller DMA-sized tiles.
- Power/perf trade-offs: use core-specific optimized path (POPCNT vs NEON) via runtime CPU feature detection.
- Edge cases: non-multiple-of-unroll N, unaligned pointers, missing POPCNT instruction — provide fallbacks.
How do you stay informed about what a function you regularly work with actually cares about and is measured on, even when you're not in the room for their planning?
Sample Answer
Direct answer
Build a standing information diet from what the partner function already produces for itself, its goals or planning document, the metrics it is measured on, and its retro or release notes, and pair that with a recurring informal check-in with one counterpart in that function. You are not trying to get invited into their planning meeting; you are trying to read what they optimize for, and occasionally confirm your read against a real person.
Structured elaboration
| Channel | Typical cadence | What it surfaces |
|---|---|---|
| Their goals or planning document (OKRs, roadmap) | Once per planning cycle | What they are formally accountable for this period |
| Dashboards or metrics they report on | Check periodically | What "good" looks like for them, in their own numbers |
| Retro notes, release notes, postmortems | As published | What is currently painful or top of mind for them |
| Recurring 1:1 with one counterpart | Biweekly or monthly | Informal context, upcoming priorities, translation of jargon |
| Occasional silent sit-in on their planning | A couple of times a year | Calibrates your read of the artifacts against how they actually talk about trade-offs |
The habit that ties these together: translate their metric into one sentence you could say back to them and have them agree it is accurate, then test that sentence the next time you talk. If you cannot state their current priority in a sentence they would sign off on, your information diet has a gap.
Worked example
Suppose you regularly partner with a support or customer-success function but are not in their planning. Their quarterly goals page (a document they publish for their own team) states the goal is "reduce median response time." Reading that before proposing a change that would meaningfully increase inbound volume lets you flag the likely trade-off to your counterpart ahead of launch, rather than finding out after the fact that you worked against their stated goal. The artifact told you what they were measured on; the counterpart conversation confirmed it was still current.
Trade-offs & pitfalls
- Relying only on artifacts risks reading a goal that is stale or aspirational and no longer reflects what the team is actually prioritizing day to day.
- Relying only on a single counterpart's opinion risks mistaking one person's take for the function's actual priority, especially if that person is not close to how the team's metrics are reviewed.
- A common miss: reading the dashboard but never validating the interpretation with anyone in that function, which produces confidently wrong assumptions that only surface when a decision already went the wrong way.
- The senior differentiator on an easy-sounding question like this is treating it as a standing habit built before you need it, rather than something you scramble to learn only after a conflict has already surfaced.
Explain how to use git blame and git annotate to investigate when a line of code was last changed and by whom. Then discuss limitations of blame (e.g., when history is rewritten via rebase/squash) and strategies to maintain traceability in projects that frequently squash or rebase before merge.
Sample Answer
Direct answer
git blame <file> (git annotate is essentially the same underlying operation, presented slightly differently) shows, for every line currently in a file, which commit last touched it, along with that commit's author and date, by walking the file's history backward line by line. It's how you answer "who wrote this and why" without opening the full commit log. Its main limitation is that it attributes a line to whichever commit last touched it, so history-rewriting operations like rebase, squash, or a repo-wide reformat can point blame at a commit that isn't the real, meaningful origin of that line.
Structured elaboration
How blame works: for each line in the current version of a file, git finds the most recent commit that changed it, and reports that commit's hash, author, date, and (with the right flags) the original line content.
Limitations under history rewriting:
- A rebase or squash that folds several small commits into one collapses each small commit's individual reasoning (its own message) into a single combined commit. Blame still points at a real, meaningful commit, but you lose the granular "why" that used to live in each separate step.
- A large mechanical commit, most commonly a repo-wide reformat or a bulk rename, makes blame point at that reformat commit for every line it touched, even though the actual logical author and reasoning is much older. This is the most disruptive case in practice, because it can bury real authorship under an unrelated, uninformative commit.
- Squashing before merge (a common practice for a clean history) means blame after the squash points at the merged commit rather than at whichever of the original WIP commits actually introduced a given line.
Strategies to maintain traceability:
git blame -wignores whitespace-only changes when attributing a line.git blame --ignore-rev <hash>, or better, a repo-wide.git-blame-ignore-revsfile (supported since Git 2.23) listing commit hashes that blame should skip over entirely when walking back, configured once withgit config blame.ignoreRevsFile .git-blame-ignore-revs. This is the standard fix for the reformat-commit problem: list the reformat commit's hash, and blame walks straight past it to the real prior change.git log --follow <file>tracks a file's history across renames, useful alongside blame since blame alone doesn't always surface a rename boundary clearly.- At the process level, even if individual "wip"/"fix typo" commits get squashed away, make sure the final squashed commit's message is itself informative (references the ticket/PR, explains the actual reasoning), so that even post-squash, blame at least points at a commit that tells you something real, rather than a generic "fixes."
Worked example
.git-blame-ignore-revs:
# Reformat entire codebase with the new formatter, 2026-03-01
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
git config blame.ignoreRevsFile .git-blame-ignore-revs
After this, git blame on a file that was touched by that reformat commit skips straight past it and attributes each line to whatever commit last meaningfully changed it before the reformat.
Trade-offs and pitfalls
.git-blame-ignore-revs only helps for tools and workflows that respect it (recent git, GitHub, GitLab all do; older tooling might not), so it's not a universal fix, more a convention worth adopting deliberately and documenting for the team. Relying purely on commit-message discipline to preserve traceability through squashes is fragile, it depends on every contributor actually writing a good final message, whereas --ignore-rev is a mechanical fix that works regardless of message quality. Neither technique restores information that was never captured in the first place, if the original small commits genuinely never explained their reasoning, squashing them just makes that gap more visible, it doesn't create the gap.
When you open a disassembly listing from objdump or GDB for a small x86-64 function, what are the main clues you use to map instructions back to the original C source, identify branches or loops, and infer the function's purpose?
Sample Answer
Work from a concrete listing
Source, compiled with gcc -O1 -g -no-pie -fcf-protection=none -fno-asynchronous-unwind-tables (-g adds debug line information, -no-pie gives fixed addresses so the listing is stable, -fcf-protection=none makes sure there are no endbr64 marker instructions at function entries, which some distributions' GCC builds emit by default and the GCC 14.4 build used here does not, and the last flag omits the unwind tables that GCC emits by default on x86-64 Linux, which do not appear in this listing either way) (GCC 14.4, x86-64, run in a linux/amd64 container under emulation) and disassembled with objdump -d -M intel -S --no-show-raw-insn (the -S option interleaves source lines, which only works because -g put line information in the file). The listing below is an excerpt of that output: the file format header, the .init, .plt and .fini sections, the Disassembly of section .text: heading and the start-up functions that come before count_char (_start, frame_dummy and others) are omitted. count_char and main are exactly as objdump printed them, with no instruction or source line removed or edited:
#include <stdio.h>
int count_char(const char *s, char c)
{
int n = 0;
for (; *s; s++)
if (*s == c)
n++;
return n;
}
int main(int argc, char **argv)
{
if (argc < 2)
return 1;
printf("%d\n", count_char(argv[1], 'a'));
return 0;
}
0000000000401140 <count_char>:
#include <stdio.h>
int count_char(const char *s, char c)
{
int n = 0;
for (; *s; s++)
401140: movzx eax,BYTE PTR [rdi]
401143: test al,al
401145: je 401179 <count_char+0x39>
int n = 0;
401147: mov edx,0x0
40114c: data16 cs nop WORD PTR [rax+rax*1+0x0]
401157: nop WORD PTR [rax+rax*1+0x0]
if (*s == c)
n++;
401160: cmp sil,al
401163: sete al
401166: movzx eax,al
401169: add edx,eax
for (; *s; s++)
40116b: add rdi,0x1
40116f: movzx eax,BYTE PTR [rdi]
401172: test al,al
401174: jne 401160 <count_char+0x20>
return n;
}
401176: mov eax,edx
401178: ret
int n = 0;
401179: mov edx,0x0
return n;
40117e: jmp 401176 <count_char+0x36>
0000000000401180 <main>:
int main(int argc, char **argv)
{
if (argc < 2)
return 1;
401180: mov eax,0x1
if (argc < 2)
401185: cmp edi,0x1
401188: jle 4011b7 <main+0x37>
{
40118a: sub rsp,0x8
printf("%d\n", count_char(argv[1], 'a'));
40118e: mov rdi,QWORD PTR [rsi+0x8]
401192: mov esi,0x61
401197: call 401140 <count_char>
40119c: mov esi,eax
40119e: mov edi,0x402004
4011a3: mov eax,0x0
4011a8: call 401030 <printf@plt>
return 0;
4011ad: mov eax,0x0
}
4011b2: add rsp,0x8
4011b6: ret
4011b7: ret
The unindented lines are the source lines that -S interleaves: each group of source text applies to the instructions that follow it, up to the next source line. Two things show in this output that a plain listing hides. The compiler has put n++ and if (*s == c) together at 401160, and it has placed for (; *s; s++) both before the loop (the first test, at 401140) and after the body (the step and test, at 40116b), which is how a for loop is laid out when the test is at the bottom. The clues, in the order they are usually useful:
1. Function boundaries and names
A symbol such as <count_char>: marks the start (it comes from the symbol table, the list of function and variable names the linker keeps in the file), and the name is already a hint. In a stripped binary (symbol table removed with strip) those names are gone: nm on the stripped copy reports no symbols, and you recover boundaries from call targets and from ret instructions instead. Calls into shared libraries still carry names (printf@plt in main), and those are the best purpose clues left. @plt refers to the procedure linkage table: a small stub in the executable that jumps to the real printf in the C library (a shared library), whose address is filled in by the dynamic loader; seeing call <name>@plt means "this function calls a library function with that name".
2. Source mapping
With debug info, objdump -S or -l (file and line numbers) gives the interleave directly. Without it, map by structure: loops, calls, constants and the order of side effects. Expect optimized code to interleave source lines out of order and to repeat one line in two places (here int n = 0; appears twice, at 401147 and at 401179, because the compiler duplicated the initialization onto both sides of the first test: once on the path that enters the loop and once on the path that skips the loop for an empty string).
3. Arguments, return value, and registers
Under System V AMD64 the first integer or pointer arguments are in rdi, rsi, rdx, rcx, r8, r9 and the result is in rax. In count_char the first instruction dereferences rdi (a pointer) and later code compares sil (the low byte of rsi) against that byte: so a char * and a char. The last thing written before ret is mov eax, edx, so edx is the return value, an int.
4. Loops and branches
A conditional jump whose target address is lower than the jump itself is a backward branch, and that is a loop. Here jne 401160 at address 401174 jumps back up, so 401160 to 401174 is the loop body. A forward conditional jump (je 401179 at the top) is a guard or early exit, and the compare or test just before it (here test al,al at 401143) is what sets the flags it reads. The instruction before the branch tells you the condition: test al, al before je/jne means "is this value zero", which on a byte loaded from a pointer is the C idiom for "end of string".
5. Instruction idioms
movzx eax, BYTE PTR [rdi](move with zero-extend) is a one-byte load widened to 32 bits: acharread.add rdi, 0x1is a pointer step of one byte:s++on achar *.cmp sil, althensete al,movzx eax, al,add edx, eaxisn += (*s == c)computed without a branch:cmpsets the flags,sete(set if equal) writes 1 intoalwhen they were equal and 0 otherwise,movzxwidens it, and the 0 or 1 is added.mov edx, 0x0isn = 0(compilers also usexor edx, edx).
6. Constants, strings and calls
In main, mov esi,0x61 loads the constant 97, which is 'a' in ASCII, and mov edi,0x402004 loads an address in .rodata (the section that holds read-only data such as string literals). objdump -s -j .rodata dumps it (the file format line, the Contents of section .rodata: heading and the trailing ASCII column ....%d.. are omitted here) as 402000 01000200 25640a00: the line starts at address 402000, so the first four bytes (01 00 02 00) are at 402000 to 402003 and the next four, 25 64 0a 00, start at 402004. Decoded as ASCII (the standard byte-to-character table), 25 is %, 64 is d, 0a is the newline and 00 is the terminator that ends a C string, so the bytes spell "%d\n", the format string for printf. Before each call the argument registers are set up, and after it the returned eax is moved into the next argument register (mov esi, eax feeds printf). Running the binary with the argument banana prints 3, matching the reading.
7. Noise to skip
data16 cs nop WORD PTR [...] and nop WORD PTR [...] are multi-byte no-ops the compiler inserts so the loop at 401160 starts on a 16-byte boundary (0x401160 is 16 times 0x40116, so its last hex digit is 0). They do nothing. Unreachable bytes after a ret or jmp are usually padding or another function.
8. Syntax flavour
objdump and gdb default to AT&T syntax (source first, % on registers); -M intel in objdump, or set disassembly-flavor intel in gdb, gives the Intel order used above (destination first). Mixing the two up reverses every operand.
Summary of the inference here
String loop that reads bytes until zero, compares each against a second char argument, counts matches, returns the count: a character-counting function.
Compare allocator strategies suitable for embedded devices: first-fit, best-fit, buddy allocator, slab allocator (slab/obj pool). For each, explain expected fragmentation behavior, runtime complexity, metadata overhead, and scenarios where it fits best on constrained microcontrollers.
Sample Answer
Recommendation first. On a part with 128 KB of RAM and a worst-case latency requirement, give every real-time path fixed-size block pools (or static allocation), use a region (arena) allocator for variable-size data that is freed all at once, and keep a general first-fit or best-fit heap, if you keep one at all, for non-real-time code with a hard cap. Use a buddy allocator only if you need variable sizes freed individually and can accept roughly a quarter of the memory lost to rounding on a size mix like this one (a single request just above a power of two loses almost half of its block). The reasoning follows, with a simulation so the numbers are reproducible.
How a boundary-tag allocator works (the base of first-fit and best-fit). (A boundary tag is the size-and-status header and footer stored at the two ends of every block.) The heap is a chain of blocks. Each block starts with a header holding its size and an "allocated" bit, and ends with a footer that repeats it. Allocate: walk the chain to a free block that is big enough, and if the leftover would still be a usable block (at least 16 bytes in the simulation below), split it: the front becomes the allocation and the rest stays free. Free: clear the allocated bit, then coalesce (merge) with neighbours: the next block's header is at p + size, and the previous block's footer is at p - 4, so both neighbours are found in constant time and merged if they are free. The footer is what makes merging with the previous block cheap. The simulation charges 8 bytes per block (header and footer, 8-byte aligned sizes). The usual target of 4 bytes per allocated block comes from keeping only the header and a "previous block is free" bit in it, and storing the footer inside the payload of free blocks, where the space is unused. First-fit takes the first block that fits; best-fit scans for the smallest block that fits. Picture a 64-byte heap holding three blocks of 16 bytes (used), 16 bytes (free) and 32 bytes (used). Freeing the first block clears its allocated bit, looks at the header at p + 16 (the next block, which is free) and merges the two into one 32-byte free block, with no search.
How a buddy allocator works. The arena is treated as one block whose size is a power of two (here 128 KiB = 2^17 bytes). Every request is rounded up to a power of two, called the block's order (order 5 = 32 bytes, the smallest block here, up to order 17 = the whole arena, so 13 orders). To allocate, take a free block of the right order. If there is none, take one of the next larger order and split it in half repeatedly: one half is kept, the other half goes on the free list of its order. Each half is the other's buddy. Because block sizes are powers of two and aligned to their own size, the buddy of the block at offset off and order o is at off ^ (1u << o): flipping one address bit finds the twin without any search. To free, check whether the buddy is also a free block of the same order; if so, merge the two into one block of the next order and repeat. At most one split or merge per order is needed, so the work is bounded by the number of orders (13 here), which is the O(log N) in the table. Counted as the simulation counts them, an allocation scans up at most 12 orders to find a free block and then splits down at most 12 times, so one allocation costs at most 12 + 12 + 1 = 25 steps; the largest seen in the runs below is 21. The price is internal waste: a 33-byte request occupies 64 bytes.
How a slab allocator and a block pool work. A fixed-block pool carves memory into equal-size blocks and keeps the free ones on a list, so alloc is "pop the first free block" and free is "push it back". A slab allocator, as used in operating-system kernels, keeps one such cache per object type (a packet, a task descriptor) and obtains memory for it a whole chunk at a time (a slab), which it can hand back when every object in the slab is free. On a microcontroller with no OS the memory is usually fixed at build time, so the practical form is the per-type pool, and the simulation below uses six pools, one per size class (a standard block size: 32, 64, 128, 256, 512 and 2048 bytes), and rounds each request up to the next class. A pool never merges or splits, so it never fragments externally, but a request is rounded up to the class size.
The simulator. The code is organised in the same order as the descriptions above: a random request generator, the boundary-tag allocator (first-fit and best-fit differ only in the best flag), the buddy allocator, the pools, and a driver. All four allocators start from the same seed and draw sizes from the same mix (400,000 operations; sizes 16 to 128 bytes 70% of the time, 129 to 512 bytes 25%, 513 to 2,048 bytes 5%) against a 128 KiB arena, keeping the reserved bytes near a target fraction of the arena. The runs are comparable but not identical request for request: the sequences drift apart within a few hundred requests, because each allocator reserves a different number of bytes per block, which changes when the run switches between allocating and freeing.
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define ARENA (128u * 1024u)
#define NLIVE 4096
#define NOPS 400000
/* ---------- workload: deterministic, same seed for every allocator ---------- */
static uint32_t rng = 12345;
static uint32_t rnd(void) { rng = rng * 1664525u + 1013904223u; return rng >> 8; }
static uint32_t pick_size(void)
{
uint32_t r = rnd() % 100;
if (r < 70) return 16 + rnd() % 113; /* 16..128 */
if (r < 95) return 129 + rnd() % 384; /* 129..512 */
return 513 + rnd() % 1536; /* 513..2048 */
}
/* ---------- boundary-tag allocator: 4-byte header + 4-byte footer per block ---------- */
static uint8_t heap[ARENA];
#define HDR(p) (*(uint32_t *)(heap + (p))) /* size | alloc bit, at block start */
#define FTR(p) (*(uint32_t *)(heap + (p) + BSZ(p) - 4)) /* copy of header, at block end */
#define BSZ(p) (HDR(p) & ~1u)
#define ISA(p) (HDR(p) & 1u)
static void set_blk(uint32_t p, uint32_t sz, uint32_t a) { HDR(p) = sz | a; FTR(p) = sz | a; }
static void bt_init(void) { memset(heap, 0, sizeof heap); set_blk(0, ARENA, 0); }
static uint64_t steps_total, alloc_calls; static uint32_t steps_max, last_steps;
static int32_t bt_alloc(uint32_t req, int best)
{
uint32_t need = (req + 8 + 7) & ~7u; /* payload + header + footer, 8-aligned */
if (need < 16) need = 16;
int32_t pick = -1; uint32_t pick_sz = 0, steps = 0;
for (uint32_t p = 0; p < ARENA; p += BSZ(p)) {
steps++;
if (!ISA(p) && BSZ(p) >= need) {
if (!best) { pick = (int32_t)p; pick_sz = BSZ(p); break; }
if (pick < 0 || BSZ(p) < pick_sz) { pick = (int32_t)p; pick_sz = BSZ(p); }
}
}
last_steps = steps; steps_total += steps; alloc_calls++; if (steps > steps_max) steps_max = steps;
if (pick < 0) return -1;
uint32_t p = (uint32_t)pick;
if (pick_sz - need >= 16) { set_blk(p, need, 1); set_blk(p + need, pick_sz - need, 0); } /* split */
else set_blk(p, pick_sz, 1);
return pick;
}
static void bt_free(uint32_t p)
{
uint32_t sz = BSZ(p);
if (p + sz < ARENA && !ISA(p + sz)) sz += BSZ(p + sz); /* merge with next */
if (p > 0) { uint32_t prev_sz = *(uint32_t *)(heap + p - 4) & ~1u;
if (!(*(uint32_t *)(heap + p - 4) & 1u)) { p -= prev_sz; sz += prev_sz; } } /* merge with previous */
set_blk(p, sz, 0);
}
static void bt_stats(uint32_t *total_free, uint32_t *largest, uint32_t *blocks)
{
*total_free = *largest = *blocks = 0;
for (uint32_t p = 0; p < ARENA; p += BSZ(p)) { (*blocks)++;
if (!ISA(p)) { *total_free += BSZ(p); if (BSZ(p) > *largest) *largest = BSZ(p); } }
}
/* ---------- buddy allocator: 32-byte minimum, orders 5..17, 1-byte side table per unit ---------- */
#define MINO 5
#define MAXO 17
static uint8_t bstate[ARENA >> MINO]; /* 0 = not a block start, else order+1 (free) or 0x80|order+1 (used) */
static int32_t bnext[ARENA >> MINO], bprev[ARENA >> MINO], bhead[MAXO + 1];
static void bl_push(uint32_t off, int o) { uint32_t i = off >> MINO; bnext[i] = bhead[o]; bprev[i] = -1;
if (bhead[o] >= 0) { bprev[bhead[o]] = (int32_t)i; }
bhead[o] = (int32_t)i; bstate[i] = (uint8_t)(o + 1); }
static void bl_remove(uint32_t off, int o) { uint32_t i = off >> MINO;
if (bprev[i] >= 0) bnext[bprev[i]] = bnext[i]; else bhead[o] = bnext[i];
if (bnext[i] >= 0) { bprev[bnext[i]] = bprev[i]; }
bstate[i] = 0; }
static void bd_init(void) { memset(bstate, 0, sizeof bstate); for (int o = 0; o <= MAXO; o++) bhead[o] = -1; bl_push(0, MAXO); }
static int32_t bd_alloc(uint32_t req, uint32_t *got)
{
int o = MINO; while ((1u << o) < req) o++;
int s = o, steps = 0; while (s <= MAXO && bhead[s] < 0) { s++; steps++; }
if (s > MAXO) { last_steps = (uint32_t)steps; return -1; }
uint32_t off = (uint32_t)bhead[s] << MINO; bl_remove(off, s);
while (s > o) { s--; steps++; bl_push(off + (1u << s), s); } /* split, keep the lower half */
bstate[off >> MINO] = (uint8_t)(0x80 | (o + 1)); *got = 1u << o;
steps_total += (uint64_t)steps + 1; alloc_calls++; if ((uint32_t)steps + 1 > steps_max) steps_max = (uint32_t)steps + 1;
last_steps = (uint32_t)steps + 1; return (int32_t)off;
}
static void bd_free(uint32_t off)
{
int o = (bstate[off >> MINO] & 0x7f) - 1;
while (o < MAXO) {
uint32_t buddy = off ^ (1u << o); uint8_t st = bstate[buddy >> MINO];
if (st != (uint8_t)(o + 1)) break; /* buddy not a free block of this order */
bl_remove(buddy, o); if (buddy < off) off = buddy; o++; /* merge */
}
bl_push(off, o);
}
/* ---------- fixed-block pools: one pool per size class, carved from the arena ---------- */
static const uint32_t cls_size[6] = { 32, 64, 128, 256, 512, 2048 };
static const uint32_t cls_bytes[6] = { 16384, 24576, 24576, 24576, 16384, 24576 }; /* 128 KiB in total */
static int32_t pfree[6][4096]; static int pn[6];
static void pool_init(void) { for (int c = 0; c < 6; c++) { pn[c] = 0; for (uint32_t k = 0; k < cls_bytes[c] / cls_size[c]; k++) pfree[c][pn[c]++] = (int32_t)k; } }
/* ---------- driver ---------- */
struct live { int32_t off; uint32_t req, got; int cls; };
static struct live L[NLIVE]; static int nl;
static unsigned target_pct = 55;
static void run(const char *name, int kind)
{
rng = 12345; nl = 0; steps_total = steps_max = alloc_calls = 0;
if (kind <= 1) bt_init(); else if (kind == 2) bd_init(); else pool_init();
uint64_t fails = 0, tries = 0, live_req = 0, live_got = 0; uint32_t target = ARENA * target_pct / 100;
for (int i = 0; i < NOPS; i++) {
int do_alloc = (live_got < target) || nl == 0;
if (do_alloc && nl < NLIVE) {
uint32_t req = pick_size(), got = 0; int32_t off = -1; int cls = -1; tries++;
if (kind <= 1) { off = bt_alloc(req, kind); if (off >= 0) got = BSZ((uint32_t)off); }
else if (kind == 2) off = bd_alloc(req, &got);
else { for (cls = 0; cls < 6 && cls_size[cls] < req; cls++) ; if (pn[cls] > 0) { off = pfree[cls][--pn[cls]]; got = cls_size[cls];
steps_total++; alloc_calls++; if (steps_max < 1) steps_max = 1; } }
if (off < 0) { fails++; continue; }
L[nl++] = (struct live){ off, req, got, cls }; live_req += req; live_got += got;
} else if (nl > 0) {
int k = (int)(rnd() % (uint32_t)nl); struct live v = L[k]; L[k] = L[--nl];
live_req -= v.req; live_got -= v.got;
if (kind <= 1) bt_free((uint32_t)v.off); else if (kind == 2) bd_free((uint32_t)v.off); else pfree[v.cls][pn[v.cls]++] = v.off;
}
}
uint32_t tf = 0, big = 0, nb = 0;
if (kind <= 1) bt_stats(&tf, &big, &nb);
else if (kind == 2) { for (int o = MINO; o <= MAXO; o++) for (int32_t i = bhead[o]; i >= 0; i = bnext[i]) { tf += 1u << o; if ((1u << o) > big) big = 1u << o; } }
else { for (int c = 0; c < 6; c++) { tf += (uint32_t)pn[c] * cls_size[c]; if (pn[c] > 0 && cls_size[c] > big) big = cls_size[c]; } }
printf("%-10s fail %5.2f%% steps/alloc mean %7.1f max %5u end: free %6u B, largest %6u B, internal waste %4.1f%%\n",
name, 100.0 * (double)fails / (double)tries, alloc_calls ? (double)steps_total / (double)alloc_calls : 0.0, steps_max,
tf, big, live_got ? 100.0 * (double)(live_got - live_req) / (double)live_got : 0.0);
}
int main(int argc, char **argv)
{
if (argc > 1) target_pct = (unsigned)atoi(argv[1]);
printf("arena %u B, %d ops, live-set target %u%% of arena\n", ARENA, NOPS, target_pct);
run("first-fit", 0); run("best-fit", 1); run("buddy", 2); run("size pools", 3);
return 0;
}
Reading the code.
rndis a small linear congruential generator (a formula that produces a repeatable pseudo-random sequence), seeded the same for every allocator, so every run is repeatable and starts from the same requests.pick_sizedraws the 70/25/5 percent size mix.HDR(p)reads the 4-byte header at heap offsetp: the block size with the allocated flag in the lowest bit (sizes are multiples of 8, so that bit is free).BSZmasks the flag off,ISAextracts it,FTR(p)is the same value stored in the last 4 bytes of the block, andset_blkwrites both.bt_allocrounds the request up to include the 8 bytes of tags. Theforloop walks block to block by adding each block's size top;stepscounts blocks visited. Withbest == 0it stops at the first big-enough free block, otherwise it walks the whole chain remembering the smallest fit. It then splits if at least 16 bytes would remain.bt_freemerges with the next block by looking atp + sz, and with the previous block by reading its footer atp - 4, which is the footer trick from the description above.- In the buddy code
bstate[i]records, for each 32-byte unit that starts a block, the order plus one (and the high bit0x80when used).bd_allocfinds the smallest non-empty order at or above the request and splits down, pushing the upper half of each split onto the free list of its order.bd_freecomputesoff ^ (1u << o), checks inbstatethat the buddy is a free block of exactly ordero, merges, and moves up an order. Thebnext/bprevarrays hold the free-list links; a real implementation stores them inside the free blocks themselves, so its metadata is only the 1 byte per unit state table counted in the table below. - The pools keep a stack of free block numbers per class (
pfree); alloc pops one and free pushes it back. runkeeps a list of live allocations, allocates while live bytes are below the target and otherwise frees a random live block, then prints the statistics.
Built with gcc -O1 -g -Wall -Wextra -fsanitize=address,undefined alloc_sim.c -o sim in a gcc:14 container (no warnings), ./sim 55 and ./sim 85 print:
arena 131072 B, 400000 ops, live-set target 55% of arena
first-fit fail 0.00% steps/alloc mean 218.6 max 560 end: free 58880 B, largest 43792 B, internal waste 5.4%
best-fit fail 0.00% steps/alloc mean 415.8 max 527 end: free 58760 B, largest 32152 B, internal waste 5.1%
buddy fail 0.00% steps/alloc mean 1.2 max 21 end: free 58304 B, largest 32768 B, internal waste 25.1%
size pools fail 10.52% steps/alloc mean 1.0 max 1 end: free 59008 B, largest 2048 B, internal waste 28.8%
arena 131072 B, 400000 ops, live-set target 85% of arena
first-fit fail 1.27% steps/alloc mean 366.8 max 857 end: free 20536 B, largest 1872 B, internal waste 7.0%
best-fit fail 0.55% steps/alloc mean 650.8 max 741 end: free 19104 B, largest 1816 B, internal waste 6.1%
buddy fail 0.23% steps/alloc mean 1.1 max 21 end: free 19424 B, largest 2048 B, internal waste 25.9%
size pools fail 45.61% steps/alloc mean 1.0 max 1 end: free 19840 B, largest 256 B, internal waste 27.7%
How to read it: steps counts blocks visited for first-fit and best-fit (an implicit chain, meaning blocks are found by walking from one block to the next; an explicit free list links only the free blocks, which shortens the walk but its worst case still grows with the number of free blocks), and search plus split steps for the buddy allocator. internal waste is reserved bytes minus requested bytes, as a share of reserved bytes, including headers. largest is the biggest free block at the end: it is the size of the biggest request that could still succeed, and compared with free it is the measure of external fragmentation. A worked reading: best-fit's mean of 415.8 steps against first-fit's 218.6 at 55% load is not a quirk. First-fit stops at the first block that fits, on average about halfway along the chain, while best-fit has to visit every block to be sure it found the smallest fit, so its mean is roughly the number of blocks in the chain. The size pools' 10.52% failures at 55% load come from the guessed partition: a class runs out of blocks while others sit idle, which the free column (59,008 B) hides because it is spread across classes. This is one synthetic workload with one seed; the pool partition is a deliberate guess (equal-ish shares, not tuned to the size mix) to show its failure mode.
Comparison.
| Strategy | Time per alloc / free | Fragmentation | Metadata | Fits when |
|---|---|---|---|---|
| First-fit | scan up to every block (560 steps at most in this run, 219 mean at 55% load) | external; at 85% load the largest free block fell to 1,872 B of 20,536 B free | 8 B per block here, 4 B with footers only in free blocks | non-real-time code, mixed sizes, modest load |
| Best-fit | whole-chain scan every time: 416 mean at 55% load | external; leaves small slivers; no better than first-fit in the 55% run (largest 32,152 B against 43,792 B), fewer failures in the 85% run (0.55% against 1.27%) | same as first-fit | when wasted bytes matter more than time |
| Buddy | O(log N): at most 21 steps seen here (bound 25 with 13 orders) | external fragmentation is real at high load (at 85% load the largest free block is 2,048 B of 19,424 B free, but no request exceeds 2,048 B, so only 0.23% failed), plus 25% to 26% internal waste from power-of-two rounding | 1 B per 32-B unit in a side table here (4,096 B, 3.1%) | variable sizes, individual frees, bounded time, spare RAM |
| Slab / block pool | O(1): 1 step | none inside a pool; the risk is one pool running dry while others have space | zero for allocated blocks (the free list lives in free blocks) | identical objects (packets, events, messages) |
Failure behaviour differs more than speed. First-fit and best-fit fail late and unpredictably, after a long run, when no single block is big enough even though free is large (the 85% rows). Buddy fails when the requested order has no free block. A pool fails exactly when its own blocks run out, which is predictable and testable, but the 10.52% at a 55% load shows what a wrong partition costs: the partition must come from the real size histogram plus headroom, and pool high-water marks must be logged in the field.
What to do on this part.
- Identical objects: one pool per type (
pool_alloc(pool)andpool_free(pool, p)take a pool, not a size). Alloc and free are a pop and a push on a free list, constant time with a few instructions, so the latency bound is the same every time. Protect the list with a very short interrupt-masked section if an ISR also allocates. - Variable-size, same lifetime: a region allocator: bump a pointer, free everything by resetting it. O(1), no fragmentation, no per-block metadata.
- Variable-size, independent lifetimes, bounded time: buddy, or several size-class pools.
- Real-time tasks: static allocation at start-up and no allocation after it.
- Whatever you pick: a malloc-like API that returns NULL on failure, a count of failures and a high-water mark (the largest number of blocks or bytes in use at any moment), a poison fill on free (overwrite freed memory with a recognisable pattern such as
0xDDso a stale pointer shows up) to catch use-after-free, and a test that runs the worst-case load.
What flips it. If the size mix is wide and unknown, the pools' guessed partition fails (as shown), and the buddy allocator's roughly 25% rounding loss may be the cheaper price. If the code never needs a deadline and runs for minutes, a first-fit heap with a cap is simpler than any of these.
Design a 30-60-90 day onboarding plan for a new hire joining your team. What do you prioritize in each phase, and how do you know they're on track?
Sample Answer
Direct answer
A good 30-60-90 plan moves someone from learning the environment, to contributing under supervision, to owning outcomes independently, with the phase boundaries defined by demonstrated behavior (what they can do unsupervised) rather than by the calendar alone. Track it with a small number of concrete, visible outputs per phase so "on track" is something you can point to, not just a feeling.
The three phases, by what changes
- Days 1-30 (learn and observe): environment setup, codebase or domain orientation, shadowing, and one small real contribution rather than a toy task, so the first change is real but low-risk.
- Days 31-60 (contribute under guidance): own a medium-sized piece of work end to end with a mentor available for review and unblocking, not doing it alongside them line by line.
- Days 61-90 (own outcomes): lead something (a project, an on-call rotation, a smaller onboarding task for the next hire) with the mentor as a backstop, not a co-pilot.
How you know they're on track
- Define the signal per phase in advance, not retroactively: for phase 1, did they reproduce the environment and ship one small real change without major help; for phase 2, is their review feedback shrinking in volume and severity over successive changes; for phase 3, can they make a reasonable decision alone and only escalate the genuinely hard calls.
- Check in on cadence (weekly early on, less frequent later) rather than waiting for day 30, 60, or 90 to find out something drifted three weeks ago.
Adjusting the plan for real constraints
- Limited training resources: when there's no dedicated ramp-up bandwidth (no spare mentor hours, no formal training material), lean harder on asynchronous artifacts: written runbooks, recorded walkthroughs, a curated list of the most representative recent changes, and a lighter-touch weekly sync instead of daily pairing. The phases stay the same; what changes is how much is self-serve versus live.
- Cross-skill ramp: if someone hired primarily for one skill set is expected to also ship in an adjacent one by day 90 (for example, a backend-focused hire expected to ship frontend work), that adjacent skill needs its own explicit milestone inside the plan, not an assumption it'll happen by osmosis. Concretely: days 1-30 stays focused on their strong area to build early confidence and trust; days 31-60 introduces the adjacent skill on a small, well-scoped, low-risk piece with close review; days 61-90 has them own something end to end in the new area, even if smaller in scope than their core-skill ownership.
Worked example
For a new hire joining an established codebase with a small team and no dedicated onboarding budget (the limited-resources case), the 30-60-90 looked like: days 1-30, self-serve environment setup using a written runbook plus a single half-day pairing session, culminating in one small, real bug fix; days 31-60, ownership of one medium feature with async review as the main touchpoint, and a short weekly 15-minute sync instead of daily check-ins; days 61-90, the new hire wrote the onboarding runbook update for the next person, which served double duty as both a real deliverable and a check on whether they actually understood the system well enough to explain it. Being on track was tracked by a short checklist per phase (environment reproducible, first fix merged with normal review effort, feature shipped with review comments trending down) rather than a single blanket "how's it going" check-in.
Trade-offs and pitfalls
- Treating the day boundaries as fixed calendar dates rather than behavioral milestones creates false confidence; someone can hit day 60 without actually being ready for phase-3 ownership, and pushing them into it anyway sets them up to fail.
- Under-supporting the adjacent-skill ramp (assuming a backend engineer will "pick up" frontend without an explicit milestone) is a common way cross-skill onboarding quietly fails; it needs the same structure as the primary skill, just smaller in scope.
- Compressing the plan under limited training resources by cutting phase 1 short (rushing into real ownership before the environment and codebase are understood) trades a faster-looking ramp for more review overhead and rework later.
Describe how you would implement fault handlers (for example hardfault, busfault, memmanage on Cortex-M) to aid debugging and to improve reliability. Include techniques for capturing register state, safe logging, minimizing footprint, and strategies for recovering or failing safely.
Sample Answer
Clarify goal
Provide fault handlers that capture useful state with minimal footprint, perform safe logging (or mark for post-mortem), and either recover when safe or fail into a known safe state.
Approach (high-level)
- Capture CPU state (stack pointer, LR, PC, xPSR, core registers) immediately in the fault handler.
- Use safe, non-blocking logging: store data to a reserved RAM crash buffer or toggle an LED; avoid heap, printf, or interrupts.
- Minimize code/stack use and make handlers reentrant-safe.
- Provide recovery options: guarded restart, safe shutdown, or halt with watchdog to reset.
Concrete techniques & example
- In assembly or C, read stacked registers saved by hardware on exception entry. Store to a fixed RAM struct (no malloc). Example extracting stacked PC/LR:
// Called from a naked handler that provides stack pointer
void HardFault_Handler_C(uint32_t *stacked_regs) {
// stacked_regs: r0, r1, r2, r3, r12, lr, pc, xPSR
crash_buffer.pc = stacked_regs[6];
crash_buffer.lr = stacked_regs[5];
crash_buffer.xpsr = stacked_regs[7];
// capture other registers via manual reads if needed
safe_mark_crash(); // set flag in RAM / checksum
system_safe_halt();
}
- Use a small CRC/checksum to validate buffer on next boot and store a timestamp or reset reason.
- For BusFault/MemManage read CFSR, HFSR, BFAR/MMFAR to identify faulting address.
Safe logging & footprint
- Pre-allocate a fixed-size crash buffer in no-init RAM, keep writes atomic (disable interrupts briefly).
- Avoid syscalls, dynamic allocations, or peripheral drivers that may be in bad state.
- Optionally use DMA to persist log to flash or external store during reboot sequence—but use conservative retries.
Recovery & fail-safe
- If fault is transient and stack/registers sane, attempt controlled restart of offending task or peripheral.
- Else: disable outputs, put actuators to safe state, feed watchdog to force a reset or enter infinite loop to allow external monitor to act.
- Ensure bootloader reads crash buffer and reports via serial/telemetry, and include a mechanism to prevent boot loops (failcount threshold).
Trade-offs / best practices
- Maximize post-mortem data while keeping handler tiny.
- Prefer RAM buffer + boot-time reporter over complex in-handler I/O.
- Test handlers by fault injection and validate buffer integrity across resets.
Tell me about a time when you had to get two or more teams with different priorities to deliver the same business outcome. How did you establish the shared goal, surface disagreements early, and keep the work moving when trade-offs had to be made?
Sample Answer
Situation: I led a launch that needed Product, Engineering, and Support to deliver the same outcome, which was reducing customer setup time.
Task: Each team had different priorities, so I needed one shared goal and a way to surface trade-offs early.
Action: I started with a single business metric, then broke it into team-level commitments. Product owned the user flow, Engineering owned reliability, and Support owned readiness. I held a weekly cross-functional checkpoint where each team shared risks, not just status. When conflicts came up, I made the trade-off explicit. For example, we chose to delay one nonessential feature so we could simplify onboarding and reduce support tickets.
Result: The teams stayed aligned, the launch shipped with fewer surprises, and the process made future collaboration easier because everyone knew how decisions would be made.
The key lesson was that shared outcomes work best when the goal is visible, disagreements are discussed early, and trade-offs are decided openly instead of being left to drift.
Design an atomic, zero-copy, high-throughput API for an application that streams sensor samples from an ISR to a processing thread using circular DMA buffers. Detail how you would coordinate buffer ownership, detect overruns, expose completed packet lengths, and allow the processor to process one buffer while DMA continues filling another.
Sample Answer
Clarify constraints & goals
- Zero-copy: CPU must get direct pointers into DMA memory (no memcopy).
- Atomic: ownership transfer must be race-free between ISR/DMA and processing thread.
- High-throughput: minimal ISR work, lock-free, pipeline DMA → process concurrently.
- Platform: circular DMA (hardware writes samples to ring buffer), CPU may have caches.
High-level design
- Use N fixed-size buffer segments in circular DMA memory (N≥2). Each segment has a small header:
- seq (uint32), len (uint32), flags (enum: FREE | WRITING | READY | READING).
- DMA writes into segments sequentially; when finishing a segment it atomically updates len then flags=READY and increments seq.
- Processing thread claims READY segments by atomically swapping flags to READING, reads len and processes buffer, then sets flags=FREE.
- ISR only updates descriptor when DMA completes (or DMA engine raises IRQ); ISR does minimal work: mark READY and poke a queue/semaphore.
Concurrency & atomicity
- Use 32-bit atomic stores for flags and seq; ordering enforced with memory barriers (compiler + CPU fence).
- Ownership transition pattern:
- DMA path: write data -> write len (release fence) -> store flags=READY (atomic release).
- Processor: atomically CAS flags from READY→READING (acquire fence) then read len and data pointer.
- No locks required; CAS ensures single owner.
Overrun detection
- Maintain seq numbers per segment. When DMA is about to start writing a segment whose flags != FREE, increment an overrun counter and set an OVERRUN flag; optionally drop oldest segment (backpressure) or signal higher layer.
- Also track distance between DMA write index and CPU read index; if distance ≥ N => overrun.
Expose completed packet lengths
- DMA must write len (actual bytes) before setting READY. Processor reads len after acquiring ownership. Provide accessor: (void* ptr, uint32_t len, uint32_t seq).
Allow concurrent processing
- With N≥2, DMA writes to upcoming FREE segments while CPU processes READING segment. Use ring indices: dma_tail (where DMA writes), cpu_head (where CPU reads). DMA never writes into a segment not FREE.
Cache coherency
- On cacheable systems, ISR/DMA path must: clean data cache lines before setting READY (DMA->memory) and processor must invalidate cache lines before reading (or use non-cacheable DMA region). Use platform cache ops around ownership transitions.
Example C structs & snippets
typedef enum { FREE=0, WRITING=1, READY=2, READING=3 } buf_state_t;
typedef struct {
atomic_uint32_t seq;
atomic_uint32_t len;
atomic_uint32_t state;
uint8_t data[SEGMENT_SIZE];
} segment_t;
segment_t ring[N];
atomic_uint32_t dma_idx = 0;
atomic_uint32_t cpu_idx = 0;
DMA completion ISR:
// ISR context: minimal
segment_t *s = &ring[idx];
atomic_thread_fence_release();
atomic_store(&s->len, actual_len);
atomic_store(&s->state, READY); // single atomic store for publish
signal_processor();
Processor claim:
if (atomic_compare_exchange_strong(&s->state, READY, READING)) {
atomic_thread_fence_acquire();
uint32_t len = atomic_load(&s->len);
process(s->data, len);
atomic_store(&s->state, FREE);
}
Trade-offs & tuning
- N larger → more buffering, higher latency; N small → risk overruns.
- Use scatter-gather DMA descriptors for variable-length packets.
- If strict latency, choose N=2 (double-buffer); for throughput, N≥4.
- For hard real-time, avoid heap; pre-allocate ring and keep ISR minimal.
This design gives atomic, zero-copy ownership handoff, explicit lengths, overrun detection via seq/state, and permits DMA and processor to work concurrently.
Three periodic tasks run under rate-monotonic scheduling on one core: A with a 1 ms execution time every 4 ms, B with 2 ms every 6 ms, and C with 3 ms every 12 ms. Is the set schedulable? State the utilisation test you would apply first, what it tells you here, and what you do when it is inconclusive.
Sample Answer
Verdict: schedulable. The quick utilisation test is inconclusive, and the exact test (response-time analysis) shows every task meets its deadline. The numbers below come from the script shown in step 3, run in a python:3.12-slim container.
Step 1: RM priorities. Rate-monotonic scheduling (RM) gives the shortest period the highest priority: A (period 4 ms) > B (6 ms) > C (12 ms). Deadline equals period for all three.
Step 2: the Liu and Layland utilisation test. Utilisation U is the sum of execution time over period: 1/4 + 2/6 + 3/12 = 0.25 + 0.3333 + 0.25 = 0.8333 (5/6). Liu and Layland (1973) showed that n periodic tasks under RM are certainly schedulable if U <= n(2^(1/n) - 1). For n = 3 the bound is 3(2^(1/3) - 1) = 0.7798. Here 0.8333 > 0.7798, so the test fails to confirm schedulability. It is a sufficient test: passing proves schedulable, failing proves nothing, and the set may still be fine. (U < 1 is a necessary condition, so it is not overloaded.)
Step 3: when inconclusive, run exact response-time analysis. The worst-case response time R of a task (from release to completion) is the smallest fixed point of R = C + sum over higher-priority tasks j of ceil(R / T_j) * C_j. Start with R = C and iterate until it stops changing; stop with failure if R exceeds the deadline. The worst case is a release at the critical instant, when all tasks release together.
| Task | C | T = D | Iteration sequence for R | R | Meets D? |
|---|---|---|---|---|---|
| A | 1 | 4 | 1, 1 | 1 | yes |
| B | 2 | 6 | 2, 3, 3 | 3 | yes |
| C | 3 | 12 | 3, 6, 7, 9, 10, 10 | 10 | yes (10 <= 12) |
For C: start at 3. Next: 3 + ceil(3/4)1 + ceil(3/6)2 = 6. Then 3 + 21 + 12 = 7. Then 3 + 21 + 22 = 9. Then 3 + 31 + 22 = 10. Then 3 + ceil(10/4)*1 + ceil(10/6)*2 = 3 + 3 + 4 = 10, a fixed point, and 10 ms is within the 12 ms deadline with 2 ms of slack.
Script and output. Run with Python 3.12:
from math import ceil
tasks = [("A", 1, 4), ("B", 2, 6), ("C", 3, 12)] # name, C (ms), T = D (ms), highest priority first
def response_time(i):
c, d = tasks[i][1], tasks[i][2]
r, seq = c, [c]
while True:
nxt = c + sum(ceil(r / t) * cj for _, cj, t in tasks[:i])
seq.append(nxt)
if nxt == r or nxt > d:
return nxt, seq
r = nxt
print("U =", round(sum(c / t for _, c, t in tasks), 4), "bound =", round(3 * (2 ** (1 / 3) - 1), 4))
for i, (name, c, t) in enumerate(tasks):
r, seq = response_time(i)
print(name, "iterations", seq, "R =", r, "meets deadline:", r <= t)
U = 0.8333 bound = 0.7798
A iterations [1, 1] R = 1 meets deadline: True
B iterations [2, 3, 3] R = 3 meets deadline: True
C iterations [3, 6, 7, 9, 10, 10] R = 10 meets deadline: True
Tracing one 12 ms window by hand with all tasks released at 0 (A runs 0-1, B 1-3, C 3-4, A 4-5, C 5-6, B 6-8, A 8-9, C 9-10), C completes at 10 ms, matching the analysis. The CPU is idle for the last 2 of 12 ms, consistent with U = 5/6.
Would EDF help? EDF (earliest deadline first, dynamic priority) is schedulable if and only if U <= 1 for deadline equal to period, and 0.8333 <= 1, so EDF also schedules this set. Here EDF is not needed: RM already works. EDF matters for sets such as two tasks (C=2, T=5) and (C=4, T=7), where U = 0.9714 and RM fails (response time 8 > 7) while EDF succeeds.
What to do in practice. Sign off using the exact test, not the bound, and record the margin (here 2 ms on C). Re-run the analysis whenever a worst-case execution time estimate changes, because the margin is small. If the exact test had failed, the levers are: cut a C (optimise or split work), lengthen a period, make periods harmonic (each period divides the next, which lets RM reach U = 1), or move to EDF.
Want to create your own tailored preparation guide using our deep research?
Get Started for FreeInterview-Ready Courses
Visual-first, interactive, structured learning paths
Browse Embedded Developer jobs
AI-enriched listings across hundreds of company career pages
Explore Jobs