TOP C Interview Question and answers Author : Kaushik Roy
1. Explain the compilation process in C.
The compilation process transforms your C source code into an executable file in four main
stages.
1. Preprocessing: The preprocessor (cpp) handles directives that start with #. It expands
macros (#define), includes header files (#include), and handles conditional compilation
(#ifdef). The output is an expanded source code file (e.g., with a .i extension).
2. Compilation: The compiler proper (e.g., cc or gcc) takes the preprocessed file and
translates it into assembly language code specific to the target processor architecture.
The output is an assembly file (e.g., with a .s extension).
3. Assembly: The assembler (as) converts the assembly code into machine code (binary
0s and 1s). It creates an object file (e.g., with a .o extension), which contains the
machine code along with metadata for linking.
4. Linking: The linker (ld) merges one or more object files with necessary code from C
standard libraries (like printf functionality) to create a single, complete executable file that
the operating system can load and run.
Flowchart of the C Compilation Process:
Code snippet
graph TD
A[Source Code (.c)] -->|1. Preprocessing| B(Expanded Source Code .i);
B -->|2. Compilation| C(Assembly Code .s);
C -->|3. Assembly| D[Object File (.o)];
D -->|4. Linking| E[Executable File (a.out)];
F[Library Files .lib/.a] --> E;
2. What is the difference between an Object file and an Executable file?
● An Object file (.o or .obj) is the output of the assembler. It contains machine code for a
specific source file but isn't fully ready to run. It may have unresolved references to
functions or variables defined in other files or libraries (e.g., a call to printf is just a
placeholder).
● An Executable file (e.g., a.out or .exe) is the final output of the linker. It's a complete,
runnable program where all references have been resolved, and all necessary code from
libraries has been linked together.
3. Difference between a compiler and a cross-compiler?
● A Compiler is a program that runs on a specific platform (e.g., an x86 PC running
Windows) and creates executable code that also runs on the same platform.
● A Cross-Compiler is a program that runs on one platform (the host, e.g., an x86 PC)
but generates executable code for a different platform (the target, e.g., an ARM-based
embedded device). This is essential for embedded systems development.
4. Does preprocessing happen in the case of Assembly Language Code?
No, preprocessing is a specific feature of languages like C and C++. The preprocessor is part of
the C compilation toolchain and is not invoked when assembling an assembly language file
directly. Assembly language has its own directives (sometimes called pseudo-ops), but they are
handled by the assembler itself, not a separate preprocessor stage.
5. What is a program?
A program is a set of instructions written in a programming language that tells a computer what
to do. Once compiled, it becomes an executable file that the computer's CPU can understand
and execute to perform a specific task.
6. Discuss C programming standardization.
Standardization ensures that C code is portable and behaves consistently across different
compilers and platforms.
● K&R C (1978): The original version from the book "The C Programming Language" by
Kernighan and Ritchie. It served as an informal standard for years.
● C89 / C90 (ANSI C): The first official standard for C, published by ANSI in 1989 and
adopted by ISO in 1990. It formalized many aspects of the language.
● C99 (1999): A major revision that added new features like inline functions, flexible array
members, // single-line comments, and new data types like long long int.
● C11 (2011): Introduced features like multi-threading support (_Thread_local), improved
Unicode support, and generic selections (_Generic).
● C17 / C18 (2018): Primarily a bug-fix release for C11, it doesn't introduce new features
but clarifies existing specifications.
● C23 (Expected): The next planned standard, adding features like true and false
keywords, typeof, and digit separators (e.g., 1'000'000).
7. What are C comments and how are they used (// and /* */)?
Comments are text in the source code that is ignored by the compiler. They are used to explain
the code to human readers.
● /* ... */ (Multi-line comment): Can span multiple lines. Everything between /* and */ is
ignored.
● C
/* This is a multi-line comment.
It explains the purpose of the
following function. */
int add(int a, int b) {
return a + b;
}
●
●
● // ... (Single-line comment): Ignores everything from // to the end of the line. Officially
added in C99.
● C
int x = 10; // Initialize x with the value 10
●
●
8. What is the difference between declaration and definition of a variable?
● Declaration: A declaration introduces a variable's name and type to the compiler but
does not allocate memory for it. It's like saying, "this variable exists somewhere." The
extern keyword is used for this. A variable can be declared many times.
● Definition: A definition actually allocates memory for the variable. It's the point where
the variable is created. A variable can only be defined once.
// In file1.c
extern int x; // Declaration of x (tells compiler x is an int defined elsewhere)
// In file2.c
int x = 10; // Definition of x (allocates memory for x and initializes it)
9. Explain different types of C data types.
Data types specify the type of data a variable can hold. Sizes and ranges can vary by system
architecture (e.g., 16-bit vs. 64-bit). The values below are typical for modern 64-bit systems.
Data Type Storage Size (bytes) Value Range
char 1 -128 to 127 or 0 to 255
unsigned char 1 0 to 255
short 2 -32,768 to 32,767
unsigned short 2 0 to 65,535
int 4 -2,147,483,648 to 2,147,483,647
unsigned int 4 0 to 4,294,967,295
long 8 Very large range (system dependent)
long long 8 Even larger range
float 4 Single-precision floating point number
double 8 Double-precision floating point number
long double 16 Extended-precision floating point number
10. Explain the sizeof operator in C.
sizeof is a compile-time unary operator used to determine the size, in bytes, of a variable or a
data type. It's useful for memory allocation and ensuring portability.
int a;
int size_a = sizeof(a); // Returns size of variable 'a' (usually 4)
int size_int = sizeof(int); // Returns size of data type 'int' (usually 4)
double arr[10];
int size_arr = sizeof(arr); // Returns 10 * sizeof(double) = 80
To find the size of a variable without using sizeof, you can use a macro:
#define MY_SIZEOF(type) (char *)((type *)0 + 1) - (char *)((type *)0)
// Usage:
printf("Size of int is %ld\n", MY_SIZEOF(int)); // Prints 4
11. What are variables, and how are they defined and initialized in C?
A variable is a named storage location in memory that holds a value, which can be changed
during program execution.
● Definition: Specifies the data type and name. Memory is allocated.
● C
int age; // Defines an integer variable named 'age'
●
●
● Initialization: Assigns an initial value to the variable at the time of definition.
● C
int age = 30; // Defines and initializes 'age' with the value 30
●
●
12. What are the rules for naming a variable in C?
1. A variable name can contain letters (a-z, A-Z), digits (0-9), and the underscore _
character.
2. The first character must be a letter or an underscore. It cannot be a digit.
3. Keywords (like int, while, return) cannot be used as variable names.
4. Variable names are case-sensitive (age and Age are different variables).
5. There's no rule on length, but it's good practice to keep them descriptive and
manageable.
13. What are Lvalues and Rvalues?
● Lvalue ("locator value"): Represents a memory location. It can appear on the
left-hand side or right-hand side of an assignment. Variables are lvalues. Think of it as
an object that has an address.
● C
int x = 10; // 'x' is an lvalue
x = 20; // OK: 'x' is on the left
●
●
● Rvalue ("read value"): A temporary value that does not have a persistent memory
location. It can only appear on the right-hand side of an assignment. Literals (like 10),
the results of expressions (like x + y), and function returns are rvalues.
● C
int y = x; // 'x' is an lvalue, but here its content (rvalue) is read
// 10 = x; // ERROR: 10 is an rvalue and cannot be assigned to
●
●
14. How is a signed negative number stored in memory?
Signed negative integers are typically stored using Two's Complement representation.
1. Start with the positive binary representation of the number.
2. Invert all the bits (0s become 1s, and 1s become 0s). This is the One's Complement.
3. Add 1 to the result.
Example: Storing -5 in an 8-bit char
1. Positive 5: 0000 0101
2. Invert bits: 1111 1010 (One's Complement)
3. Add 1: 1111 1011 (Two's Complement representation of -5)
The most significant bit (MSB) acts as the sign bit: 0 for positive, 1 for negative.
15. What is the difference between \n and \r?
● \n (Newline / Line Feed - LF): Moves the cursor down to the next line.
● \r (Carriage Return - CR): Moves the cursor to the beginning of the current line.
Different operating systems use them differently for line endings:
● Linux/Unix/macOS: Use only \n.
● Windows: Use a combination \r\n.
● Classic Mac OS: Used only \r.
In C, \n is usually translated to the OS's native line ending convention automatically when
writing to text files.
16. Explain the Memory Layout of C Programs.
When a C program is loaded into memory, it is typically organized into several distinct
segments:
1. Text Segment (.text): Contains the compiled machine code (the program's instructions).
It's read-only to prevent the program from accidentally modifying its own instructions.
2. Initialized Data Segment (.data): Stores global, static, and constant variables that are
initialized with a specific value before the program starts. For example, int global_var =
10;.
3. Uninitialized Data Segment (.bss): Stores global and static variables that are
uninitialized or initialized to zero. The system initializes all memory in this segment to
zero at the start. For example, int global_uninit;.
4. Heap: Region of memory used for dynamic memory allocation. You manage this
memory manually using malloc(), calloc(), realloc(), and free(). The heap grows upwards
(towards higher memory addresses).
5. Stack: Used for static memory allocation. It stores local variables, function
parameters, and return addresses. Memory is allocated and deallocated automatically as
functions are called and return (in a Last-In, First-Out manner). The stack grows
downwards (towards lower memory addresses).
High Memory Address
+-----------------------------+
| Command Line Args |
| & Environment Vars |
+-----------------------------+
| Stack | <-- Grows Down
| | |
| V |
+------------------------------+
| |
| ... |
| |
+------------------------------+
| ^ |
| | |
| Heap | <-- Grows Up
+------------------------------+
| BSS | (Uninitialized Data)
+-------------------------------+
| Data | (Initialized Data)
+-------------------------------+
| Text | (Code)
+-------------------------------+
Low Memory Address
17. What is dynamic memory allocation in C?
Dynamic memory allocation is a way to request memory from the heap during program runtime.
This is useful when the amount of memory needed is not known at compile time.
● malloc(size_t size): Allocates a single block of memory of the specified size in bytes. It
returns a void* pointer to the first byte of the allocated space. The memory is not
initialized (it contains garbage values).
● C
int *arr = (int*)malloc(10 * sizeof(int)); // Allocate space for 10 integers
●
●
● calloc(size_t num, size_t size): Allocates memory for an array of num elements, each of
size bytes. It returns a void* pointer. The key difference from malloc is that calloc
initializes the allocated memory to zero.
● C
int *arr = (int*)calloc(10, sizeof(int)); // Allocate and zero-initialize space for 10 integers
●
● If calloc cannot find a contiguous memory block of the requested size, it fails and returns
NULL. It will not allocate a non-contiguous block.
● realloc(void *ptr, size_t new_size): Changes the size of a previously allocated memory
block (pointed to by ptr) to new_size. It might move the block to a new location if
necessary.
● C
arr = (int*)realloc(arr, 20 * sizeof(int)); // Resize the array to hold 20 integers
●
●
● free(void *ptr): Releases a block of memory previously allocated by malloc, calloc, or
realloc, returning it to the heap for future use.
● C
free(arr); // Free the allocated memory
arr = NULL; // Good practice to avoid dangling pointers
●
●
What is the return value of malloc(0)?
The behavior is implementation-defined. It can either return a NULL pointer or a valid pointer
that cannot be dereferenced but must be passed to free() to avoid a memory leak. It's best to
avoid calling malloc(0).
18. What is memory leakage in ‘C’? How can it be reduced?
A memory leak occurs when a program allocates memory from the heap (using malloc, calloc,
etc.) but loses the pointer to that memory without freeing it. This memory becomes unusable for
the rest of the program's life, effectively reducing the amount of available memory.
How to reduce it:
1. Always free() what you malloc(): For every call to malloc, calloc, or realloc, ensure
there is a corresponding call to free().
2. Assign NULL after free(): After freeing a pointer, set it to NULL. This prevents it from
becoming a dangling pointer.
3. Be careful with pointers in loops: Ensure memory allocated inside a loop is freed
correctly, especially if the pointer is reassigned in each iteration.
4. Use tools: Use memory analysis tools like Valgrind to detect leaks automatically.
19. What is dynamic memory fragmentation?
Fragmentation occurs when free memory on the heap is broken into small, non-contiguous
blocks over time as memory is allocated and freed.
● External Fragmentation: There is enough total free memory on the heap to satisfy a
request, but it is not available in a single, contiguous block. For example, the heap has
100 KB of free space, but it's split into ten 10 KB chunks, so a request for 20 KB will fail.
● Internal Fragmentation: A memory allocator provides a block of memory that is larger
than what the program requested. The unused space within that allocated block is
wasted. For example, if a program requests 29 bytes, the allocator might give it a
32-byte block, wasting 3 bytes.
20. What is virtual memory?
Virtual memory is an operating system memory management technique that gives a program
the illusion that it has its own private, contiguous block of main memory (called an address
space), which is much larger than the actual physical RAM available. The OS manages the
mapping between the program's virtual addresses and the actual physical addresses in RAM,
often using disk space (a swap file or page file) as an extension of RAM.
This allows for:
● Running programs larger than physical memory.
● Protecting processes from interfering with each other's memory.
● Efficient process creation.
21. What is stack overflow? What are its causes?
A stack overflow is a runtime error that occurs when the call stack, which has a limited size,
runs out of space. This usually happens when the stack pointer exceeds the stack bound.
Common Causes:
1. Infinite Recursion: A recursive function that never reaches its base case, continuously
making calls to itself and consuming stack space with each call.
2. C
void overflow() {
overflow(); // Calls itself forever
}
3.
4.
5. Very Deep Recursion: A recursive function that has too many levels of nesting, even if
it has a valid base case.
6. Large Local Variables: Declaring very large arrays or structures as local variables
inside a function can consume a significant amount of stack space in a single go.
7. C
void large_array() {
char buffer[2000000]; // 2MB array on the stack may cause overflow
}
8.
9.
22. Where exactly are pointer variables and structures stored in memory?
● Pointer Variables: A pointer is just a variable that stores a memory address. The
pointer variable itself is stored according to its storage class.
○ A global or static pointer is stored in the Data/BSS segment.
○ A local pointer (declared inside a function) is stored on the Stack.
○ A pointer allocated dynamically (e.g., int **p = malloc(...)) is stored on the Heap.
● Structures: Like pointers, the location of a structure variable depends on how it's
declared.
○ A global or static struct is stored in the Data/BSS segment.
○ A local struct variable is stored on the Stack.
○ A struct allocated dynamically (struct my_struct *s = malloc(...)) resides on the
Heap.
23. What is the difference between heap and stack memory?
Feature Stack Heap
Allocation Automatic, by the compiler. Manual, by the programmer
(malloc, calloc).
Deallocation Automatic (when function Manual (free).
returns).
Speed Very fast allocation and Slower due to complex
deallocation. management.
Size Limited, relatively small. Much larger, limited by system
memory.
Flexibility Fixed size (compile-time). Resizable (runtime).
Management LIFO (Last-In, First-Out). No fixed order, can lead to
fragmentation.
Access CPU manages it directly via Accessed via pointers.
stack pointer.
Risk Stack overflow. Memory leaks, fragmentation.
24. What is a pointer variable? How do you declare one?
A pointer is a special variable that stores the memory address of another variable. It "points
to" the location of the other variable.
You declare a pointer by prefixing its name with an asterisk (*).
// Declaration of a pointer to an integer
int *ptr;
// Example of use:
int var = 10;
ptr = &var; // The '&' operator gets the address of 'var'
// Now 'ptr' holds the memory address of 'var'
// To access the value at the address stored in ptr, use '*' (dereference operator)
printf("Value of var is %d\n", *ptr); // Prints 10
25. Explain the concept of pointer data types and their purpose.
The data type of a pointer is crucial because it tells the compiler two things:
1. Size of the data it points to: When you dereference a pointer (e.g., *ptr), the compiler
knows how many bytes to read from that memory address. For an int *, it reads 4 bytes;
for a char *, it reads 1 byte.
2. How to perform pointer arithmetic: When you increment a pointer (e.g., ptr++), the
address it holds is increased by the size of the data type it points to.
○ int *ptr; ptr++ will increase the address by sizeof(int) (usually 4 bytes).
○ char *ptr; ptr++ will increase the address by sizeof(char) (1 byte).
This ensures that the pointer correctly moves from one element to the next in an array.
26. How are pointers used in embedded programming?
Pointers are fundamental in embedded systems for:
1. Accessing Hardware Registers: Peripherals (like timers, GPIOs, UARTs) are
controlled by writing to and reading from specific memory-mapped registers. Pointers are
used to directly access these fixed memory addresses.
2. C
// Define a pointer to the GPIO Port A Data Register at a fixed address
#define GPIOA_DATA_R (*((volatile unsigned int *)0x400043FC))
// Set bit 2 of Port A
GPIOA_DATA_R |= (1 << 2);
3.
4.
5. Efficient Data Handling: Passing large structures or arrays to functions using pointers
avoids copying large amounts of data onto the stack, saving memory and time.
6. Implementing Communication Protocols: Managing data buffers for protocols like
UART, SPI, and I2C.
7. Dynamic Data Structures: Although less common due to memory constraints, they can
be used for things like message queues in an RTOS.
27. What is a function pointer? How do you declare one? Where can they
be used?
A function pointer is a pointer that stores the memory address of a function's executable code.
It can be used to call the function it points to.
Declaration Syntax:
return_type (*pointer_name)(parameter_types);
// Function to be pointed to
int add(int a, int b) {
return a + b;
}
int main() {
// Declare a function pointer 'p_func' that can point to a function
// which takes two ints as parameters and returns an int.
int (*p_func)(int, int);
// Assign the address of the 'add' function to the pointer
p_func = &add; // or just p_func = add;
// Call the function using the pointer
int result = (*p_func)(10, 5); // Traditional way
int result2 = p_func(20, 10); // Simpler, modern way
printf("Result 1: %d\n", result); // Prints 15
printf("Result 2: %d\n", result2); // Prints 30
}
Uses:
1. Callback Functions: Passing a function as an argument to another function (e.g., for
event handling or in sorting algorithms).
2. Jump Tables / State Machines: Creating an array of function pointers to implement a
state machine, where the state determines which function to call.
3. Late Binding: Deciding which function to call at runtime.
28. What is a void pointer (generic pointer)? What are its advantages?
A void pointer (void *) is a generic pointer that can point to any data type. It doesn't have an
associated type, so it's just a raw memory address.
Advantages:
1. Flexibility: A single function can accept a void * argument to operate on different data
types. Library functions like malloc, free, and memcpy use void * for this reason.
2. Genericity: Allows for the creation of generic data structures and algorithms.
Limitations:
● You cannot directly dereference a void * because the compiler doesn't know the size
of the data it points to.
● You cannot perform pointer arithmetic on it.
You must explicitly cast a void * to another pointer type before using it.
void printValue(void* data, char type) {
if (type == 'i') {
printf("Value: %d\n", *((int*)data)); // Cast to int* then dereference
} else if (type == 'c') {
printf("Value: %c\n", *((char*)data)); // Cast to char* then dereference
}
}
int main() {
int i = 10;
char c = 'A';
printValue(&i, 'i');
printValue(&c, 'c');
}
29. What is a null pointer? What are its uses?
A NULL pointer is a special pointer that is guaranteed not to point to any valid object or
function. It represents an "empty" or "invalid" pointer. In C, it is typically defined as ((void*)0).
Uses:
1. Initialization: It's good practice to initialize pointers to NULL to ensure they don't hold a
garbage address.
2. C
int *ptr = NULL;
3.
4.
5. Error Checking: Functions that return a pointer (like malloc) often return NULL to
indicate failure. The caller should always check for this.
6. C
int *arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
// Handle memory allocation failure
}
7.
8.
9. Sentinel Values: In data structures like linked lists, the next pointer of the last node is
set to NULL to mark the end of the list.
30. What is a null character (\0)?
The null character (\0) is a character with an ASCII value of 0. It is used to mark the end of a
string in C. All C string functions (like strlen, strcpy) rely on this null terminator to know where
the string ends.
It is different from a NULL pointer.
● \0: A character (char) with a value of zero.
● NULL: A pointer (void *) with a value of zero.
31. What are dangling pointers? How can you solve this problem?
A dangling pointer is a pointer that points to a memory location that has been deallocated
(freed) or is no longer valid (e.g., the address of a local variable that has gone out of scope).
Using a dangling pointer can lead to unpredictable behavior, crashes, or security vulnerabilities.
Causes:
1. De-allocating memory:
2. C
int *p = (int *)malloc(sizeof(int));
free(p);
// Now 'p' is a dangling pointer.
*p = 10; // Undefined Behavior!
3.
4.
5. Returning address of a local variable:
6. C
int* create_var() {
int local_var = 5;
return &local_var; // Address of local_var is invalid after function returns
}
int *ptr = create_var(); // ptr is a dangling pointer
7.
8.
Solution:
The best practice is to assign NULL to a pointer immediately after freeing the memory it points
to. A NULL pointer is safe to free() again and easy to check.
free(p);
p = NULL; // Now 'p' is not dangling.
32. What is a wild pointer?
A wild pointer is a pointer that has not been initialized. It points to some random, unknown
memory location. Using a wild pointer (reading from or writing to it) is extremely dangerous as it
can corrupt memory, overwrite critical data, or crash the program.
int *p; // 'p' is a wild pointer because it has not been initialized.
*p = 10; // CRASH! This writes 10 to a random memory location.
Solution: Always initialize your pointers, either to a valid address or to NULL.
int *p = NULL; // Safe initialization
33. Differentiate between a constant pointer and a pointer to a constant.
This is a classic question about the placement of the const keyword. Read the declaration from
right to left.
1. Pointer to a Constant (const int *p or int const *p)
○ "p is a pointer (*p) to an int that is const."
○ The data pointed to cannot be changed through this pointer.
○ The pointer itself can be changed to point to something else.
2. C
int x = 10, y = 20;
const int *p = &x;
// *p = 15; // ERROR: Cannot change the value pointed to.
p = &y; // OK: Can change where the pointer points.
3.
4.
5. Constant Pointer (int * const p)
○ "p is a const pointer (* const p) to an int."
○ The pointer itself is constant and cannot be changed to point to another
location. It must be initialized at declaration.
○ The data it points to can be changed.
6. C
int x = 10, y = 20;
int * const p = &x;
// p = &y; // ERROR: Cannot change the pointer itself.
*p = 15; // OK: Can change the value at the address.
7.
8.
34. Explain a constant pointer to a constant.
const int * const p or int const * const p
● Reading from right to left: "p is a const pointer (* const p) to an int that is const."
● This means neither the pointer nor the data it points to can be changed. It's
completely locked down.
int x = 10, y = 20;
const int * const p = &x;
// *p = 15; // ERROR: Cannot change the value.
// p = &y; // ERROR: Cannot change the pointer.
35. Can we have a volatile pointer?
Yes. Similar to const, volatile can be applied to the pointer itself or the data it points to.
1. Pointer to a volatile variable (volatile int *p): The integer being pointed to is volatile.
The compiler won't optimize reads/writes to *p. The pointer p itself is not volatile.
2. Volatile pointer (int * volatile p): The pointer p itself is volatile. Its address value can be
changed by external means. The data it points to, *p, is not considered volatile.
3. Volatile pointer to a volatile variable (volatile int * volatile p): Both the pointer and
the data it points to are volatile.
This is rare but can be necessary in complex scenarios like a pointer stored in memory that is
shared between a main process and an interrupt service routine.
36. Are the expressions *ptr++ and ++*ptr the same?
No, they are completely different due to operator precedence and associativity.
● *ptr++ (Post-increment pointer): The ++ (postfix) has higher precedence than * but is
evaluated after the expression.
1. The expression first evaluates to *ptr (the value at the current address).
2. As a side effect, the pointer ptr is then incremented to point to the next element.
● C
int arr[] = {10, 20};
int *ptr = arr;
int val = *ptr++; // val becomes 10, ptr now points to 20
●
●
● ++*ptr (Pre-increment value): The * and ++ (prefix) have the same precedence and
right-to-left associativity.
1. The pointer is dereferenced first: *ptr.
2. The value at that address is then incremented: ++(*ptr).
3. The pointer ptr does not move.
● C
int arr[] = {10, 20};
int *ptr = arr;
int val = ++*ptr; // arr[0] becomes 11, val becomes 11. ptr still points to arr[0].
●
●
37. How does taking the address of a local variable result in unoptimized
code?
When you take the address of a local variable (&my_var), you create a pointer to it. The
compiler must then assume that this pointer could be used to modify the variable's value in
ways it cannot predict (this is called "aliasing").
Because the variable's value could change unexpectedly via the pointer, the compiler is forced
to disable certain optimizations. For example, it cannot cache the variable's value in a CPU
register for fast access, because the value in memory might be altered. It must perform a fresh
memory read each time the variable is accessed, which is slower. This is why the register
keyword suggests not taking the address of the variable.
38. How can a double pointer be useful?
A double pointer (a pointer to a pointer, e.g., int **p) is useful in several scenarios:
1. Modifying a Pointer in a Function: To change the actual pointer variable passed into a
function, you must pass its address (a pointer to that pointer). This allows the function to
make the original pointer point to a new location.
2. C
void allocate_memory(int **ptr, int size) {
*ptr = (int *)malloc(size * sizeof(int)); // Modifies the original pointer
}
int main() {
int *my_ptr = NULL;
allocate_memory(&my_ptr, 10); // Pass the address of my_ptr
// Now my_ptr points to the allocated memory
free(my_ptr);
}
3.
4.
5. Array of Pointers (e.g., Array of Strings): A common way to represent an array of
strings is as an array of char * pointers. A pointer to this array would be a char **.
6. C
char *names[] = {"Alice", "Bob", "Charlie"};
char **p_names = names;
7.
8.
9. Dynamic 2D Arrays: Creating a 2D array on the heap often involves creating an array
of pointers, where each pointer then points to a row of data.
39. Explain the const keyword in C.
The const keyword is a type qualifier that makes a variable read-only. It tells the compiler and
other programmers that the variable's value should not be changed after initialization.
● Properties:
○ Must be initialized at the time of declaration.
○ Any attempt to modify a const variable will result in a compile-time error.
● When to use:
○ For defining mathematical or physical constants (const double PI = 3.14159;).
○ In function parameters, to indicate that the function will not modify the data
passed to it via a pointer (void print_data(const int *data)). This is a contract.
● Placement in Memory:
○ Global const variables are typically stored in a read-only part of the Text
Segment.
○ Local const variables are usually stored on the Stack, but the compiler will still
enforce the read-only rule.
● const declarations:
○ const int a; or int const a;: Declares a constant integer a.
○ const int *a;: Declares a as a pointer to a constant integer (value can't change,
pointer can).
○ int * const a;: Declares a as a constant pointer to an integer (pointer can't
change, value can).
○ const int * const a;: Declares a as a constant pointer to a constant integer
(neither can change).
40. Explain the volatile keyword in C.
The volatile keyword is a type qualifier that tells the compiler that a variable's value may change
at any time by something outside the scope of the program.
● Purpose: It prevents the compiler from applying optimizations that assume the variable's
value is stable. Specifically, it forces the compiler to generate code that reads the
variable from main memory every time it is accessed and writes to main memory every
time it is assigned. It prevents caching the variable's value in a CPU register.
● When to use:
1. Memory-Mapped Peripheral Registers: Hardware registers in embedded
systems can change value at any time (e.g., a status register). They must be
declared volatile.
2. Global variables modified by an ISR: If a global variable is modified by an
Interrupt Service Routine (ISR), it must be volatile so that the main program sees
the updated value.
3. Global variables in multi-threaded applications: When variables are shared
between threads and modified without proper locking mechanisms.
Syntax:
// A volatile integer
volatile int status_register;
// A pointer to a volatile integer (common for hardware registers)
volatile uint32_t * const UART0_DR = (uint32_t *)0x4000C000;
// A volatile pointer
int * volatile p;
41. What is the difference between const and volatile?
Feature const volatile
Purpose To prevent the programmer To prevent the compiler from
from changing a value. optimizing away accesses to a
variable.
Who No one (ideally). The value Something outside the program's
changes it? is fixed. control (hardware, ISR, another
thread).
Effect The variable becomes Forces every access to be a
read-only in the code. read/write from/to main memory.
Analogy A "read-only" sign for the A "this value is unpredictable"
programmer. warning for the compiler.
42. Can a variable be both constant and volatile?
Yes. This combination is common in embedded systems. It means:
● const: My program should not (and cannot) write to this variable. It is "read-only" from
my code's perspective.
● volatile: The value of this variable can be changed by an external agent (like hardware),
so the compiler must not optimize away reads of it.
Use Case: A hardware status register that is read-only. For example, a timer's current count
register. Your program can only read it, but the hardware is constantly changing its value.
// Example: A read-only hardware timer counter register at a fixed memory address
volatile const uint32_t * const TIMER_COUNT_REG = (uint32_t *)0x40010024;
// Usage
uint32_t current_time = *TIMER_COUNT_REG; // Read the current time
// *TIMER_COUNT_REG = 0; // COMPILE ERROR: Cannot write to a const location.
43. Explain Storage Classes in C.
Storage classes in C determine a variable's scope (where it's accessible), visibility (linkage),
and lifetime (how long it exists in memory).
There are four storage classes: auto, register, extern, and static.
● auto:
1. The default storage class for local variables.
2. Scope: The block in which it is defined.
3. Lifetime: Created when the block is entered, destroyed when the block is exited.
4. Storage: Stack.
● C
void func() {
auto int x = 10; // 'auto' keyword is optional here
}
●
●
● register:
1. A suggestion to the compiler to store the variable in a fast CPU register instead
of on the stack.
2. Scope & Lifetime: Same as auto.
3. Storage: CPU register (if the compiler agrees). If not, it becomes an auto
variable.
4. You cannot take the address (&) of a register variable.
5. Cannot be used for global variables because they need a permanent memory
location.
● C
void func() {
register int i; // Good for loop counters
}
●
●
● extern:
1. Used to declare a global variable that is defined in another file. It tells the
compiler that the variable exists but is defined elsewhere. It does not allocate
memory.
2. Scope: Global (entire program).
3. Lifetime: Entire program execution.
4. Storage: Data/BSS segment.
● C
// file1.c
int global_var = 100;
// file2.c
extern int global_var; // Declares that global_var is defined elsewhere
void func() {
printf("%d\n", global_var); // Accesses the variable from file1.c
}
●
●
● static:
The static keyword has two different meanings depending on where it's used.
1. Inside a function (static local variable):
■ Scope: The block in which it is defined.
■ Lifetime: Preserves its value between function calls. It is initialized
only once and exists for the entire duration of the program.
■ Storage: Data/BSS segment.
2. C
void counter() {
static int count = 0; // Initialized only the first time
count++;
printf("Count: %d\n", count);
}
// Calling counter() multiple times will print 1, 2, 3...
3.
4.
5. Outside a function (static global variable or function):
■ It restricts the visibility (linkage) of the variable or function to the file in
which it is defined. It cannot be accessed from other files using extern.
This is also called internal linkage.
6. C
// file1.c
static int secret = 42; // Cannot be accessed from file2.c
static void private_func() { /* ... */ } // Cannot be called from file2.c
7.
8.
● static in header files: Declaring a static variable in a header file is generally bad
practice. It creates a separate, independent copy of that variable in every .c file that
includes the header, which is usually not what you want.
44. Explain "Call by Value" and "Call by Reference".
This describes how arguments are passed to functions.
● Call by Value: The function receives a copy of the argument's value. Any changes
made to the parameter inside the function do not affect the original argument in the
calling code. This is the default mechanism in C for all data types except arrays.
● C
void modify(int x) {
x = 100; // Modifies the copy, not the original
}
int main() {
int a = 10;
modify(a);
printf("%d\n", a); // Prints 10
}
●
●
● Call by Reference (simulated in C using pointers): The function receives the address
of the argument. By dereferencing this address (pointer), the function can access and
modify the original argument in the calling code.
● C
void modify(int *p) {
*p = 100; // Modifies the original value at the address
}
int main() {
int a = 10;
modify(&a); // Pass the address of 'a'
printf("%d\n", a); // Prints 100
}
●
●
45. What are "Inline functions" in C?
An inline function is a function for which the compiler is requested to replace the function call
with the actual code of the function itself at the call site. This avoids the overhead of a function
call (pushing/popping from the stack, jumping).
● How: Use the inline keyword. It's a request, not a command; the compiler can ignore it.
● Advantages:
○ Faster execution for small, frequently called functions by eliminating function
call overhead.
● Disadvantages:
○ Increased code size (code bloat) if the function is large or called many times,
which can lead to worse cache performance.
○ Not suitable for large functions or recursive functions.
C
inline int max(int a, int b) {
return (a > b) ? a : b;
}
int main() {
int m = max(10, 20); // Compiler might replace this with: int m = (10 > 20) ? 10 : 20;
}
46. What is a reentrant function?
A reentrant function is a function that can be interrupted in the middle of its execution and then
safely called again ("re-entered") before the first call has completed. Once the second call
finishes, the first call can resume from where it was interrupted without any issues.
This is critical in multi-threaded environments and for functions that can be called from an
Interrupt Service Routine (ISR).
Rules for a function to be reentrant:
1. It must not use static or global non-const variables.
2. It must not return the address of a static variable.
3. It must work only on the data provided to it by its callers (i.e., its arguments).
4. It must not call other non-reentrant functions.
// Reentrant
int add(int a, int b) {
return a + b;
}
// NOT Reentrant
int counter = 0;
int get_and_increment() {
return counter++; // Modifies a global variable
}
47. What happens when recursive functions are declared inline?
Most compilers will refuse to inline a recursive function. Inlining works by replacing a call with
the function's body. For a recursive function, this would mean pasting the function's body inside
itself, leading to infinite code expansion at compile time. The compiler is smart enough to detect
this and will just treat the inline keyword as a normal function call.
48. Why cannot arrays be passed by values to functions?
When you pass an array to a function, you are not passing the entire array by value. Instead,
the array name "decays" into a pointer to its first element. So, you are actually passing this
pointer by value.
There are two main reasons for this design in C:
1. Efficiency: Passing an entire array by value would require copying every single element
of the array onto the stack. This would be extremely slow and consume a lot of stack
memory, especially for large arrays.
2. Simplicity: The language was designed to be simple and efficient. Passing a pointer is a
fast and direct way to give a function access to the array's data.
// These two function declarations are identical to the compiler
void print_array(int arr[10]);
void print_array(int *arr);
49. How do you pass a function as an argument to another function
(callback)?
You do this using function pointers. A function that accepts another function as an argument is
often called a higher-order function, and the function being passed is called a callback
function. The higher-order function "calls back" the provided function to perform a specific task.
Example: A generic sort function that uses a callback to compare elements.
#include <stdio.h>
#include <stdlib.h>
// Callback function prototype
typedef int (*compare_func)(int, int);
// A simple comparison function (our callback)
int ascending(int a, int b) {
return a > b;
}
// Another comparison function (another callback)
int descending(int a, int b) {
return a < b;
}
// A higher-order function that uses a callback to sort
void bubble_sort(int arr[], int n, compare_func cmp) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
// Use the callback function to compare
if (cmp(arr[j], arr[j + 1])) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
// Pass the 'ascending' function as a callback
bubble_sort(arr, n, ascending);
printf("Sorted array in ascending order:\n");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
}
50. What is an ISR (Interrupt Service Routine)?
An Interrupt Service Routine (ISR), also known as an interrupt handler, is a special function in
a microcontroller's firmware that is executed when a specific interrupt event occurs. Interrupts
are signals generated by hardware (e.g., a timer overflow, a button press, data arrival on UART)
or software that cause the CPU to pause its current task, save its state, and execute the ISR.
After the ISR finishes, the CPU resumes its original task.
51. Can you pass any parameter to or return a value from an ISR?
No. An ISR is not called by your code directly; it's triggered by hardware. Therefore:
● You cannot pass parameters to an ISR. The hardware triggering mechanism doesn't
have a way to provide arguments. If the ISR needs data, it must access it through
shared global variables (which must be declared volatile).
● You cannot return a value from an ISR. There is no "caller" waiting for a return value.
The ISR's job is to handle the hardware event and then return control to the interrupted
code. In C, ISRs are always defined with a void return type.
52. What is interrupt latency? How can it be reduced?
Interrupt latency is the time delay between the moment a hardware interrupt is generated and
the moment the first instruction of its ISR begins to execute.
Causes of Latency:
1. The CPU must finish executing its current instruction.
2. The CPU needs to save the current context (program counter, registers) onto the stack.
3. The CPU needs to look up the address of the ISR in the Interrupt Vector Table.
4. If a higher-priority interrupt is already running, the new interrupt must wait.
5. If interrupts are globally disabled, the new interrupt must wait until they are re-enabled.
How to Reduce Latency:
1. Keep ISRs short and fast.
2. Avoid long, uninterruptible sections of code in your main program.
3. Use a processor with features designed for low latency (e.g., ARM Cortex-M's
tail-chaining).
4. Optimize compiler settings for speed.
53. What is a nested interrupt system?
A nested interrupt system is one that allows a higher-priority interrupt to preempt (interrupt) a
lower-priority ISR that is currently executing.
Rules:
● Each interrupt source is assigned a priority level.
● If an interrupt occurs while another ISR is running, the CPU compares their priorities.
● If the new interrupt has a higher priority, the current ISR is paused, and the new,
higher-priority ISR begins to execute.
● If the new interrupt has an equal or lower priority, it will be held pending until the
current ISR completes.
This ensures that the most critical events are handled with the least delay.
54. What is NVIC in ARM Cortex?
The NVIC (Nested Vectored Interrupt Controller) is a standard hardware component in ARM
Cortex-M series microcontrollers. It's a highly sophisticated interrupt controller that manages
interrupts in the system.
Key Features:
● Nested Interrupts: Manages interrupt priorities and allows higher-priority interrupts to
preempt lower-priority ones.
● Vectored Interrupts: It uses an Interrupt Vector Table where each entry is the direct
address of the corresponding ISR. This means the CPU doesn't have to waste time
figuring out which ISR to run; it can jump directly to it, reducing latency.
● Low Latency: Features like tail-chaining and late-arriving interrupt handling further
reduce the time spent on interrupt overhead.
● Programmable Priorities: Allows the programmer to set the priority level for each
interrupt source.
55. Can you use any function inside an ISR?
No, you must be very careful. Functions called from an ISR must be reentrant.
Key Considerations:
1. Reentrancy: The function must not use global/static variables in a non-atomic way and
must not call other non-reentrant functions.
2. Execution Time: Functions called from an ISR should be very short and fast.
Long-running functions will increase interrupt latency for other interrupts.
3. Blocking: Never call functions that could block or wait for an event (like delay() or some
RTOS blocking calls). An ISR must run to completion as quickly as possible.
4. printf(): Avoid using functions like printf inside an ISR. They are often not reentrant, can
be very slow, and may rely on other interrupts (like a UART transmit interrupt) which
could be disabled.
A common practice is to have the ISR do the absolute minimum work (e.g., set a flag, copy data
to a buffer) and let the main loop handle the longer processing task.
56. Can you change the interrupt priority level of Cortex-M?
Yes. The ARM Cortex-M NVIC allows you to programmatically change the priority of most
interrupts at runtime by writing to the appropriate priority registers. This provides great flexibility
in managing real-time system behavior.
57. Explain "Setter" and "Getter" functions.
Getters and Setters are functions used to retrieve and update the value of private or static
variables, respectively. This is a key concept in object-oriented programming, but the principle is
also used in C to control access to data, especially in modules or drivers.
● Getter (Accessor): A function that returns the value of a variable. Its purpose is to
provide read-only access or to perform some logic before returning the value.
● C
// In a temperature_sensor.c file
static int current_temp;
int get_temperature(void) {
// Maybe read from hardware here
return current_temp;
}
●
●
● Setter (Mutator): A function that sets or updates the value of a variable. It can perform
validation or other logic before changing the value.
● C
// In a motor_driver.c file
static int motor_speed;
void set_motor_speed(int speed) {
if (speed < 0) speed = 0;
if (speed > 100) speed = 100;
motor_speed = speed;
// Update PWM hardware with new speed
}
●
●
58. What are Macros in C?
A macro is a fragment of code that is given a name. Whenever the name is used, it is replaced
by the content of the macro by the preprocessor before compilation begins.
● Simple Macro (Object-like): Replaces a name with a value. Used for constants.
● C
#define PI 3.14159
#define BAUD_RATE 9600
●
●
● Function-like Macro: A macro that takes arguments. It provides a way to create simple
⚠️
"functions" that are expanded inline.
Always enclose arguments and the entire macro body in parentheses to avoid
operator precedence issues!
● C
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
●
●
● Conditional Macros: Used for conditional compilation.
○ #ifdef NAME: Includes the following code if NAME has been defined.
○ #ifndef NAME: Includes the following code if NAME has not been defined (often
used for header guards).
○ #if 0 ... #endif: A common way to comment out a large block of code.
● C
#ifndef MY_HEADER_H
#define MY_HEADER_H
// Header content
#endif
●
●
● Macros for Data Manipulation:
● C
// Set, clear, toggle a specific bit
#define SET_BIT(reg, bit) ((reg) |= (1U << (bit)))
#define CLEAR_BIT(reg, bit) ((reg) &= ~(1U << (bit)))
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1U << (bit)))
// Swap the two nibbles (4-bit chunks) in a byte
#define SWAP_NIBBLES(x) (((x) >> 4) | ((x) << 4))
// Swap two numbers
#define SWAP(a, b, type) do { type temp = a; a = b; b = temp; } while (0)
●
●
59. Explain the differences between Macros and Functions.
Feature Macro Function
Execution Code is substituted by the Code is compiled and
preprocessor before compilation. No called at runtime. Incurs
function call overhead. function call overhead.
Type No type checking. The preprocessor Type checking is
Checking just does text replacement, which can performed by the compiler
lead to errors. for arguments and return
value.
Code Size Can increase code size (code bloat) if Code exists in one place
used many times. only, reducing executable
size.
Debugging Difficult to debug as the macro name Easy to debug. Can step
doesn't appear in the debugger. You into the function.
see the expanded code.
Scope No scope. It's just text replacement. Has a well-defined scope.
Evaluation Arguments may be evaluated multiple Arguments are evaluated
times, leading to issues with side only once before being
effects (e.g., SQUARE(i++)). passed to the function.
60. Difference between typedef and #define.
Feature typedef #define (Macro)
Processor Handled by the compiler. Handled by the preprocessor.
Purpose Creates an alias for an Performs simple text substitution. It
existing data type. It doesn't understand C syntax.
understands C syntax.
Pointers typedef char* String; String #define String char*; String s1, s2;
s1, s2; correctly creates two expands to char* s1, s2;, which
char* pointers (s1 and s2). creates a char* pointer (s1) and a
plain char (s2).
Scope Obeys scoping rules (can Does not obey scoping rules; once
be local or global). defined, it applies until undefined.
Semicolon A typedef statement ends A #define does not.
with a semicolon.
61. Difference between a macro and const.
Feature #define (Macro) const Variable
Type Has no data type. It's just a text Has a specific data type (e.g.,
token. const int).
Memory Does not occupy memory. The Occupies memory just like a
value is substituted directly into the regular variable.
code.
Scope No scope. Global from the point of Obeys normal C scoping
definition. rules.
Debugging The symbol name is not available The symbol name is available
to the debugger. to the debugger.
Pointers You cannot take the address of a You can take the address of a
macro. const variable.
Usage Use for simple constants or Use for typed constants and
function-like macros where for passing read-only data via
performance is critical. pointers.
62. What do you mean by enumeration (enum) in C?
An enumeration (enum) is a user-defined data type that consists of a set of named integer
constants, called enumerators. It makes the code more readable and maintainable by using
meaningful names instead of magic numbers.
// Define an enumeration for days of the week
enum week { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
int main() {
// By default, MONDAY is 0, TUESDAY is 1, and so on.
enum week today = WEDNESDAY;
if (today == WEDNESDAY) {
printf("It's midweek!"); // More readable than if (today == 2)
}
}
● enum vs. Macro (#define):
○ Debugging: enum names exist in the debugger; macro names do not.
○ Typing: enum creates a new type, which can improve type checking. Macros are
just text replacement.
○ Grouping: enum groups related constants together logically.
● enum vs. typedef: They serve different purposes. enum creates a new type with named
constants. typedef creates an alias for an existing type. They are often used together:
● C
typedef enum { RED, GREEN, BLUE } color_t;
color_t my_color = GREEN;
●
●
63. Explain the difference between #define and typedef for pointer types.
This is a classic pitfall. typedef is type-aware, while #define is just text substitution.
● #define P_STUDENT STUDENT_INFO*
When you declare P_STUDENT p1, p2;, the preprocessor expands it to:
STUDENT_INFO* p1, p2;
Here, p1 is a pointer to STUDENT_INFO, but p2 is just a regular STUDENT_INFO
variable. The * only applies to p1.
● typedef STUDENT_INFO* P_STUDENT;
When you declare P_STUDENT p1, p2;, the compiler understands that P_STUDENT is
an alias for the type "pointer to STUDENT_INFO".
Therefore, both p1 and p2 are correctly declared as pointers to STUDENT_INFO.
Conclusion: Always use typedef for creating aliases for complex types like pointers.
64. What is the difference between an Array and a Linked List?
Feature Array Linked List
Memory Allocation Contiguous block of Non-contiguous memory;
memory. nodes can be anywhere.
Size Fixed size, determined at Dynamic size, can grow and
compile time. shrink at runtime.
Memory Overhead No extra memory per Extra memory per node for the
element. pointer(s).
Element Access Fast random access Slow sequential access
(O(1)) using an index (O(n)). Must traverse from the
(e.g., arr[5]). head.
Insertion/Deletion Slow (O(n)), as elements Fast (O(1)), if the node's
may need to be shifted. location is known. Only
pointers need to be updated.
65. What is the difference between an Array and a Structure?
● Array: A collection of elements of the same data type, stored in contiguous memory.
You access elements using an integer index.
● Structure: A collection of elements (called members) of different data types, grouped
under a single name. You access members using their names with the dot (.) or arrow
(->) operator.
66. What is the difference between a Structure and a Union?
The key difference is how they store their members in memory.
● Structure (struct):
○ Allocates enough memory to store all of its members.
○ Each member has its own unique memory location.
○ The total size is the sum of the sizes of its members (plus any padding).
● Union (union):
○ Allocates only enough memory to store its largest member.
○ All members share the same memory location.
○ You can only store a value in one member at a time. Writing to one member
overwrites the others.
Real-time Example:
● Use a struct when you need to store multiple, related pieces of data at the same time,
like an employee record: struct Employee { char name[50]; int id; float salary; };
● Use a union when you need to store one of several possible data types in the same
memory space, but never at the same time. This is common in embedded systems for
interpreting data from a register that can have multiple formats.
● C
// A register's value can be read as a whole 32-bit word, or as individual bytes
union Register {
uint32_t word;
uint8_t bytes[4];
};
●
●
67. How do you declare various C constructs?
● A variable: data_type variable_name; -> int score;
● An array: data_type array_name[size]; -> char name[50];
● A pointer variable: data_type *pointer_name; -> int *ptr;
● An array of pointers: data_type *array_name[size]; -> char *names[10]; (An array of 10
pointers to char)
● A pointer to an array: data_type (*pointer_name)[size]; -> int (*p_arr)[10]; (A single
pointer that points to an array of 10 ints)
● An array of structures: struct struct_name array_name[size]; -> struct student
records[30];
● A pointer to a structure: struct struct_name *pointer_name; -> struct student
*p_student;
● A pointer within a structure:
● C
struct Node {
int data;
struct Node *next; // Pointer within a structure
};
●
●
68. Explain bit-fields in structures.
Bit-fields are special members of a structure that allow you to specify their width in bits. This is
extremely useful in embedded systems for packing data tightly and for mapping structures
directly onto hardware registers where individual bits or groups of bits have specific meanings.
How to create for peripheral registers:
Imagine a control register where:
● Bits 0-3 are for a device address.
● Bit 4 is an enable flag.
● Bits 5-7 are for an operation mode.
You can create a bit-field structure to access these fields by name instead of using manual
bitwise operations.
#include <stdio.h>
#include <stdint.h>
// Define a bit-field structure to match the register layout
typedef struct {
uint32_t address : 4; // Use 4 bits for address
uint32_t enable : 1; // Use 1 bit for enable
uint32_t mode : 3; // Use 3 bits for mode
uint32_t : 24; // Unused bits (padding)
} ControlRegister_t;
int main() {
// Create a union to easily access the data as a whole word or as bit-fields
union {
ControlRegister_t fields;
uint32_t value;
} reg;
// Simulate reading a value from hardware
reg.value = 0x000000A9; // Binary: ...1010 1001
// Now access the fields by name
printf("Address: %u\n", reg.fields.address); // Prints 9 (0b1001)
printf("Enable: %u\n", reg.fields.enable); // Prints 0 (0b0)
printf("Mode: %u\n", reg.fields.mode); // Prints 5 (0b101)
// Modify a field
reg.fields.enable = 1;
printf("New register value: 0x%X\n", reg.value); // Prints 0xB9
return 0;
}
69. What is structure padding? How can it be avoided?
Structure padding is the insertion of extra, unused bytes into a structure by the compiler. This
is done to align the data members on memory addresses that are multiples of their size. CPUs
can access data much more efficiently from these "aligned" addresses. For example, a 4-byte
int is accessed faster if it starts at an address divisible by 4.
● Advantage/Disadvantage: It's an advantage for performance but a disadvantage for
memory usage, as it can waste space. It can also cause issues when interfacing with
hardware or network protocols where data layout must be exact.
● How to avoid it: You can use compiler-specific directives to control padding.
○ With GCC/Clang: Use the __attribute__((packed)) attribute.
● C
struct unpacked {
char c; // 1 byte
int i; // 4 bytes
}; // sizeof might be 8 (3 bytes padding after 'c')
struct packed {
char c; // 1 byte
int i; // 4 bytes
} __attribute__((packed)); // sizeof will be 5 (no padding)
●
○ With MSVC: Use #pragma pack(1).
● C
#pragma pack(push, 1) // Set packing to 1 byte
struct packed {
char c;
int i;
};
#pragma pack(pop) // Restore previous packing
●
●
70. Discuss advanced structure concepts.
● Nested Structures: A structure can contain another structure as one of its members.
This is used to build complex, hierarchical data models.
● C
struct Date {
int day;
int month;
int year;
};
struct Employee {
char name[50];
struct Date birth_date; // Nested structure
};
●
●
● Self-Referential Structures: A structure that contains a pointer to a structure of its own
type. This is the fundamental building block for creating linked data structures like linked
lists, trees, and graphs.
● C
// A node for a singly linked list
struct Node {
int data;
struct Node *next; // Pointer to the next node of the same type
};
●
●
71. How is typedef used with structures?
typedef is very commonly used with structures to create a shorter, cleaner alias for the structure
type. This avoids having to write struct struct_name every time you declare a variable of that
type.
Without typedef:
struct Point {
int x;
int y;
};
struct Point p1; // Have to use 'struct Point'
With typedef:
C
typedef struct {
int x;
int y;
} Point_t; // Point_t is now an alias for the structure type
Point_t p1; // Much cleaner
72. Explain union and its applicability in embedded systems.
A union is a special data type that allows you to store different data types in the same memory
location. All members of a union share the same memory space, which is large enough to hold
the largest member.
Applicability in Embedded Systems:
1. Bit Extraction / Type Punning: A union is a perfect tool for interpreting the same block
of raw data in multiple ways without type-casting. This is common for accessing
hardware registers or parsing communication protocol packets where a field can have
different meanings.
2. C
// Interpret a 32-bit value as a float or an integer
union FloatInt {
float f;
uint32_t i;
};
3.
4.
5. Saving Memory: When you need to store data that is mutually exclusive (i.e., you only
need to hold one of several types at any given time), a union saves memory compared
to a struct.
6. C
// A packet can contain either a command ID or an error code, but not both
union PacketData {
uint16_t command_id;
uint8_t error_code;
};
7.
8.
73. Explain how to create a Singly Linked List.
A singly linked list is a linear data structure made of nodes, where each node contains:
1. Data.
2. A pointer (next) to the next node in the sequence. The last node's next pointer is NULL.
#include <stdio.h>
#include <stdlib.h>
// Self-referential structure for a node
typedef struct Node {
int data;
struct Node *next;
} Node_t;
// Function to create a new node
Node_t* create_node(int data) {
Node_t *new_node = (Node_t*)malloc(sizeof(Node_t));
if (new_node == NULL) exit(1);
new_node->data = data;
new_node->next = NULL;
return new_node;
}
// Insert a node at the beginning
void insert_at_beginning(Node_t **head, int data) {
Node_t *new_node = create_node(data);
new_node->next = *head;
*head = new_node;
}
// Insert a node at the end
void insert_at_end(Node_t **head, int data) {
Node_t *new_node = create_node(data);
if (*head == NULL) {
*head = new_node;
return;
}
Node_t *last = *head;
while (last->next != NULL) {
last = last->next;
}
last->next = new_node;
}
// Delete a node from the beginning
void delete_from_beginning(Node_t **head) {
if (*head == NULL) return;
Node_t *temp = *head;
*head = (*head)->next;
free(temp);
}
// Print the list
void print_list(Node_t *head) {
while (head != NULL) {
printf("%d -> ", head->data);
head = head->next;
}
printf("NULL\n");
}
int main() {
Node_t *head = NULL; // Start with an empty list
insert_at_end(&head, 10);
insert_at_end(&head, 20);
insert_at_beginning(&head, 5); // 5 -> 10 -> 20
print_list(head);
delete_from_beginning(&head); // 10 -> 20
print_list(head);
return 0;
}
● Detect Loop in Linked List: The most famous method is Floyd's Cycle-Finding
Algorithm (also known as the "tortoise and the hare" algorithm).
1. Use two pointers, slow and fast.
2. Start both at the head.
3. In each iteration, move slow by one node and fast by two nodes.
4. If there is a loop, fast and slow will eventually meet. If fast reaches NULL, there is
no loop.
74. Explain various bitwise operations in C.
Bitwise operators work on the individual bits of integer-type data.
Let num be the variable and pos be the bit position (0-indexed).
● Set a particular bit: (Force a bit to 1)
Use the bitwise OR (|) operator.
● C
num = num | (1 << pos);
// or shorthand:
num |= (1 << pos);
●
●
● Clear a particular bit: (Force a bit to 0)
Use the bitwise AND (&) with a negated mask.
● C
num = num & ~(1 << pos);
// or shorthand:
num &= ~(1 << pos);
●
●
● Toggle a particular bit: (Flip a bit from 0 to 1 or 1 to 0)
Use the bitwise XOR (^) operator.
● C
num = num ^ (1 << pos);
// or shorthand:
num ^= (1 << pos);
●
●
● Test a bit: (Check if a bit is 1)
Use the bitwise AND (&). The result will be non-zero if the bit is set.
● C
if (num & (1 << pos)) {
// Bit is set (1)
}
●
●
● Bit Masking: Using a "mask" value to isolate, set, or clear specific bits while leaving
others untouched. The operations above are all forms of bit masking.
75. How do you find if a given number is odd or even using bitwise
operators?
An integer is even if its least significant bit (LSB) is 0. It's odd if its LSB is 1. You can test this
using bitwise AND (&) with 1.
#include <stdio.h>
void check_odd_even(int n) {
if (n & 1) {
printf("%d is Odd\n", n);
} else {
printf("%d is Even\n", n);
}
}
int main() {
check_odd_even(10); // 1010 & 0001 = 0 -> Even
check_odd_even(7); // 0111 & 0001 = 1 -> Odd
return 0;
}
76. What is the difference between Logical and Bitwise operators?
| Operators | Logical (&&, ||, !) | Bitwise (&, |, ^, ~) |
| :--- | :--- | :--- |
| Operands | Operates on boolean (true/false) expressions. Any non-zero value is true, zero is
false. | Operates on the individual bits of integer operands. |
| Result | The result is always 1 (true) or 0 (false). | The result is a new integer value, computed
bit by bit. |
| Short-Circuiting| && and || perform short-circuit evaluation. (e.g., in A && B, B is not evaluated
if A is false).| No short-circuiting. Both operands are always evaluated.|
| Example | (5 > 2) && (3 < 1) -> 1 && 0 -> 0 | 5 & 3 -> 0101 & 0011 -> 0001 (which is 1) |
77. Explain bitwise left shift (<<) and right shift (>>).
● Left Shift (<<): x << n shifts the bits of x to the left by n positions. The vacated bits on
the right are filled with 0s.
○ Application: It's a fast way to multiply by powers of 2. x << n is equivalent to
xtimes2n.
● C
int a = 5; // 0000 0101
int b = a << 2; // 0001 0100 (which is 20). 5 * 2^2 = 20.
●
●
● Right Shift (>>): x >> n shifts the bits of x to the right by n positions.
○ For unsigned types, the vacated bits on the left are filled with 0s (Logical Shift).
○ For signed types, the behavior is implementation-defined, but most compilers
perform an Arithmetic Shift, where the vacated bits are filled with the sign bit
(preserving the number's sign).
○ Application: A fast way to divide by powers of 2 (for non-negative numbers). x
>> n is equivalent to x/2n.
● C
int a = 20; // 0001 0100
int b = a >> 2; // 0000 0101 (which is 5). 20 / 2^2 = 5.
●
●
78. What is little-endian and big-endian?
Endianness refers to the order of bytes in which a multi-byte data type (like an int or float) is
stored in computer memory.
● Big-Endian: Stores the most significant byte (MSB) at the lowest memory address.
This is like how we write numbers; the "big end" comes first.
● Little-Endian: Stores the least significant byte (LSB) at the lowest memory address.
The "little end" comes first.
Example: Storing the 4-byte integer 0x12345678
● Memory Addresses: 1000 1001 1002 1003
● Big-Endian: 12 34 56 78
● Little-Endian: 78 56 34 12
Most modern PCs (Intel/AMD x86) are little-endian. Many network protocols and older
processors (like Motorola 68k) are big-endian.
79. Write a program to find if a machine is little-endian or big-endian.
The idea is to store a multi-byte number in memory and then use a character pointer to read just
the first byte. If the first byte is the least significant part of the number, the machine is
little-endian.
#include <stdio.h>
int main() {
unsigned int i = 1;
char *c = (char*)&i; // Point a char pointer to the first byte of the integer
if (*c) { // If the first byte is 1 (the LSB), it's Little Endian
printf("Machine is Little Endian\n");
} else { // If the first byte is 0 (the MSB), it's Big Endian
printf("Machine is Big Endian\n");
}
return 0;
}
80. Write a program to check if an integer is a power of 2.
A number is a power of 2 if it has exactly one bit set to 1 in its binary representation (e.g., 8 is
1000, 16 is 10000).
A clever trick is to use the expression n & (n - 1). If n is a power of two, n - 1 will be all 1s up to
that point (e.g., if n=8 (1000), n-1=7 (0111)). The bitwise AND of these two will be zero. This
also correctly handles n=0.
#include <stdio.h>
// Returns 1 if n is a power of 2, otherwise 0
int isPowerOfTwo(int n) {
// n must be positive, and (n & (n-1)) must be 0
return (n > 0) && ((n & (n - 1)) == 0);
}
int main() {
printf("Is 16 a power of 2? %s\n", isPowerOfTwo(16) ? "Yes" : "No");
printf("Is 12 a power of 2? %s\n", isPowerOfTwo(12) ? "Yes" : "No");
return 0;
}
81. Write a program to count set bits in an integer.
The Brian Kernighan's algorithm is a very efficient way to do this. The trick is that n & (n - 1)
unsets the rightmost set bit. By repeatedly applying this operation in a loop until the number
becomes 0, you can count the number of set bits.
#include <stdio.h>
int countSetBits(unsigned int n) {
int count = 0;
while (n > 0) {
n &= (n - 1); // Unset the rightmost set bit
count++;
}
return count;
}
int main() {
// 13 in binary is 1101 (3 set bits)
printf("Number of set bits in 13 is %d\n", countSetBits(13));
return 0;
}
82. Explain Looping in C.
Loops are used to execute a block of code repeatedly.
● for loop: Best when you know the number of iterations beforehand. It combines
initialization, condition checking, and modification in one line.
● C
for (int i = 0; i < 5; i++) {
printf("%d ", i);
}
●
●
● while loop: An entry-controlled loop. Best when the number of iterations is not known,
and the loop might not run at all. The condition is checked before the loop body.
● C
int i = 0;
while (i < 5) {
printf("%d ", i);
i++;
}
●
●
● do-while loop: An exit-controlled loop. The loop body is executed at least once, and the
condition is checked after the body.
● C
int i = 0;
do {
printf("%d ", i);
i++;
} while (i < 5);
●
●
83. How do you code an infinite loop in C?
The most common and often most efficient way is:
for (;;) {
// ... loop body ...
}
This is generally preferred because it's idiomatic and clearly expresses the intent of an infinite
loop. Another common way is while(1). The compiler can often optimize for(;;) more effectively.
84. Is a count-down-to-zero loop better than a count-up loop?
Yes, sometimes. A count-down-to-zero loop can be slightly more efficient on some
architectures (especially older ones and some embedded processors).
C
// Count-up
for (int i = 0; i < N; i++) { ... }
// Count-down-to-zero
for (int i = N; i > 0; i--) { ... }
The reason is that comparing a value to zero is often a dedicated, highly optimized instruction in
the CPU's instruction set (e.g., checking the "zero flag"). Comparing to a non-zero value N might
require an extra instruction to load N into a register first. On modern, highly optimizing compilers
and CPUs, the difference is usually negligible.
85. What is loop unrolling?
Loop unrolling is a compiler optimization technique that reduces the number of loop iterations
by duplicating the loop's body and adjusting the loop counter. This reduces the overhead of the
loop (the counter increment and the conditional jump) at the cost of larger code size.
Original Loop:
for (int i = 0; i < 4; i++) {
a[i] = b[i] * c;
}
Unrolled Loop:
a[0] = b[0] * c;
a[1] = b[1] * c;
a[2] = b[2] * c;
a[3] = b[3] * c;
This also helps with instruction pipelining in the CPU.
86. Swap two numbers without using a third variable.
Using Arithmetic Operators:
C
a = a + b;
b = a - b; // b = (a + b) - b = a
a = a - b; // a = (a + b) - a = b
Using Bitwise XOR (more common and safer from overflow):
The XOR swap algorithm works because x ^ x is 0 and x ^ 0 is x.
a = a ^ b;
b = a ^ b; // b = (a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a
a = a ^ b; // a = (a ^ b) ^ a = (a ^ a) ^ b = 0 ^ b = b
87. What is typedef?
typedef is a keyword in C that is used to create an alias or a synonym for an existing data type.
It does not create a new type. Its purpose is to make code more readable and portable.
Usage:
// Create an alias 'u8' for 'unsigned char'
typedef unsigned char u8;
// Create an alias 'string' for 'char*'
typedef char* string;
// Create an alias 'Node' for 'struct node_t'
typedef struct node_t {
int data;
struct node_t *next;
} Node;
// Now use the aliases
u8 status_flag = 0;
string name = "Alice";
Node *head = NULL;
88. What is conditional compilation?
Conditional compilation is a feature of the C preprocessor that allows you to include or
exclude blocks of code from compilation based on certain conditions. This is controlled by
directives like #if, #else, #elif, #endif, #ifdef, and #ifndef.
Uses:
● Writing code that is portable across different operating systems or hardware.
● Creating different versions of a program (e.g., a "debug" version and a "release"
version).
● Creating header guards to prevent multiple inclusions of the same header file.
Example:
#define DEBUG_MODE 1
void print_data(int data) {
printf("Data: %d\n", data);
#if DEBUG_MODE == 1
printf("Debug: Function print_data was called.\n");
#endif
#ifdef USER_NAME
printf("User: %s\n", USER_NAME);
#else
printf("User name not defined.\n");
#endif
}
89. Write a program to generate a Fibonacci Series.
The Fibonacci series is a sequence where each number is the sum of the two preceding ones,
starting from 0 and 1. (0, 1, 1, 2, 3, 5, 8, ...)
Iterative Approach (more efficient):
#include <stdio.h>
void fibonacci(int n) {
int t1 = 0, t2 = 1, nextTerm;
printf("Fibonacci Series: ");
for (int i = 1; i <= n; ++i) {
printf("%d, ", t1);
nextTerm = t1 + t2;
t1 = t2;
t2 = nextTerm;
}
printf("\n");
}
int main() {
fibonacci(10);
return 0;
}
90. Write a C program to reverse a string.
Using two pointers (in-place):
#include <stdio.h>
#include <string.h>
void reverse_string(char *str) {
if (!str) return;
char *start = str;
char *end = str + strlen(str) - 1;
char temp;
while (start < end) {
// Swap characters
temp = *start;
*start = *end;
*end = temp;
// Move pointers
start++;
end--;
}
}
int main() {
char my_string[] = "hello";
reverse_string(my_string);
printf("Reversed string: %s\n", my_string); // olleh
return 0;
}
Using recursion:
#include <stdio.h>
#include <string.h>
void reverse_recursive(char *str, int start, int end) {
if (start >= end) {
return; // Base case
}
// Swap characters
char temp = str[start];
str[start] = str[end];
str[end] = temp;
// Recursive call
reverse_recursive(str, start + 1, end - 1);
}
int main() {
char my_string[] = "world";
reverse_recursive(my_string, 0, strlen(my_string) - 1);
printf("Reversed string: %s\n", my_string); // dlrow
return 0;
}
91. What is the difference between ‘C’ and ‘Embedded C’?
Feature Standard C Embedded C
Environment Runs on Runs on microcontrollers with limited
general-purpose resources and no OS (or an RTOS).
computers with an OS
(Windows, Linux).
Standard Follows standards like An extension of standard C, not a
ANSI C, C99, C11. separate language. It adds features
for embedded programming.
Hardware Interacts with hardware Interacts directly with hardware
Access via OS drivers and (memory-mapped registers).
system calls.
Key N/A Bit manipulation, fixed-point
Extensions arithmetic, keywords like volatile,
#pragma directives for memory
placement.
Memory Has access to large Has very limited RAM and ROM.
virtual memory (RAM + Requires careful memory
disk). management.
I/O Uses standard libraries Interacts directly with hardware
(stdio.h) for console I/O peripherals (GPIO, UART, ADC).
(printf, scanf).
92. What is the start-up code in embedded systems?
Start-up code is a small piece of assembly code that runs immediately after the microcontroller
is powered on or reset, and before the main() function is called. Its job is to prepare the system
for the C environment.
Typical actions:
1. Disable interrupts.
2. Configure the stack pointer.
3. Initialize the C runtime environment:
○ Copy initialized data from read-only memory (Flash) to RAM (the .data section).
○ Zero out the uninitialized data section in RAM (the .bss section).
4. Initialize the main clock system.
5. Call the main() function.
93. What is the difference between RISC and CISC?
This refers to the design philosophy of a CPU's instruction set architecture.
Feature RISC (Reduced Instruction CISC (Complex Instruction Set
Set Computer) Computer)
Instruction Small, highly optimized set of Large set of instructions,
Set simple instructions. including complex ones.
Instruction Each instruction performs a A single instruction can perform a
Complexity very simple task. multi-step operation (e.g., load
from memory, do math, and store
back to memory).
Execution Most instructions execute in a Instructions can take multiple
Time single clock cycle. clock cycles.
Compiler's The compiler does more work, The compiler's job is simpler, as
Job combining simple instructions it can use dedicated complex
to perform complex tasks. instructions.
Hardware Simpler hardware, lower More complex hardware.
power consumption.
Examples ARM, MIPS, PowerPC. Intel/AMD x86. (Dominant in
(Dominant in desktops/servers).
mobile/embedded).
94. What is the difference between I2C and SPI?
Both are popular serial communication protocols for connecting peripherals to a microcontroller.
Feature SPI (Serial Peripheral I2C (Inter-Integrated Circuit)
Interface)
Speed Faster (typically > 10 MHz). Slower (Standard: 100 kHz,
Fast: 400 kHz, High-speed:
3.4 MHz).
Number of Wires 4 wires (MISO, MOSI, 2 wires (SDA, SCL).
SCLK, SS).
Communication Full-duplex (data can be Half-duplex.
sent and received
simultaneously).
Connections Master needs a separate Slaves are selected by a
Slave Select (SS) line for unique 7-bit or 10-bit address
each slave. on the shared bus.
Complexity Simpler protocol and More complex protocol
hardware. (start/stop conditions, ACKs).
Use Case High-speed data streaming Controlling multiple devices
(e.g., SD cards, displays). where speed is not critical
(e.g., sensors, EEPROMs).
Limitation of I2C: Slower speed and the bus capacitance limits the number of devices and
physical length of the bus.
95. What is segmentation fault in C?
A segmentation fault (segfault) is a specific type of error caused by a program trying to access
a memory location that it is not allowed to access. The operating system's memory
management unit (MMU) detects this illegal access and terminates the program to prevent it
from corrupting memory.
Common Causes:
1. Dereferencing a NULL pointer: int *p = NULL; *p = 10;
2. Dereferencing a dangling pointer: Accessing memory that has already been free()d.
3. Array out-of-bounds access: int arr[5]; arr[10] = 0;
4. Stack overflow: A very deep or infinite recursion can corrupt the stack, leading to an
invalid memory access.
5. Trying to write to a read-only memory location: char *str = "hello"; str[0] = 'H'; (String
literals are often in read-only memory).
96. What is the difference between a Segmentation fault and a Bus error?
While both are crashes related to memory access, they have different underlying causes.
● Segmentation Fault: A logical error. The program tried to access an address that is
outside its permitted address space. The address itself is valid in format, but the program
doesn't have permission to use it.
● Bus Error: A physical error. The program tried to access an address that is physically
invalid or impossible for the CPU to handle. The CPU puts the address on the system's
address bus, and the hardware reports that it can't service the request.
○ Common cause: An unaligned memory access on a CPU that requires
alignment (e.g., trying to read a 4-byte int from an address that is not a multiple of
4).
On many modern systems (like Linux on x86), unaligned access is handled by the hardware, so
bus errors are much rarer than segmentation faults.
97. What is a Watchdog Timer (WDT)?
A Watchdog Timer is a hardware safety timer that is used to automatically reset a
microcontroller if the main software hangs, freezes, or gets stuck in a loop.
How it works:
1. The watchdog timer is a counter that continuously counts down from a preset value.
2. If the counter reaches zero, it generates a system reset.
3. The main application software is responsible for periodically "kicking" or "feeding" the
watchdog (i.e., resetting its counter to the initial value) before it reaches zero.
4. If the software freezes, it will fail to kick the watchdog, the timer will expire, and the
system will be reset, hopefully recovering from the fault.
98. What is a DMA (Direct Memory Access) Controller?
A DMA Controller is a special piece of hardware in a system that can transfer data between
peripherals and memory, or between different memory locations, without any intervention
from the CPU.
How it works:
1. The CPU programs the DMA controller with the source address, destination address,
and the amount of data to transfer.
2. The CPU then tells the DMA to start the transfer.
3. The DMA takes control of the system bus and performs the data transfer directly.
4. Once the transfer is complete, the DMA notifies the CPU (usually with an interrupt).
Advantage: This frees up the CPU to perform other tasks while large data transfers are
happening in the background, significantly improving system performance. It's heavily used for
high-speed peripherals like ADCs, DACs, SPI, and UARTs.
99. What is RTOS (Real-Time Operating System)?
An RTOS is a specialized operating system designed for systems that need to process data and
respond to events within a strict and predictable time frame (a "deadline"). The key feature of an
RTOS is determinism, not just speed. It guarantees that a task will be completed within its
specified time constraint.
Key Concepts:
● Task/Thread: Independent units of execution.
● Scheduler: The core of the RTOS, which decides which task to run at any given time,
typically based on priority.
● Inter-Task Communication (IPC): Mechanisms like queues, semaphores, and mutexes
for tasks to communicate and synchronize with each other.
It's different from a general-purpose OS (like Windows/Linux) where fairness and throughput are
more important than strict timing guarantees.
100. What is a Makefile?
A Makefile is a special file that contains a set of rules and dependencies for building a software
project (especially in C/C++). The make utility reads this file to understand how to compile and
link the different source files into a final executable.
Benefits:
● Automation: It automates the entire build process.
● Efficiency: It can determine which files have changed since the last build and only
recompile those files and the files that depend on them, saving a lot of time on large
projects.
● Portability: It provides a standard way to build a project across different environments.
101. What is the difference between a process and a thread?
● A process is a program in execution. Each process has its own private memory space
(text, data, heap, stack), making it heavyweight. Processes are isolated from each other
by the operating system.
● A thread is a lightweight path of execution within a process. Multiple threads within the
same process share the process's memory space (text, data, heap) but have their own
separate stack and registers.
Feature Process Thread
Memory Independent, separate Shares memory space with
memory space. other threads in the same
process.
Creation Slower to create and Faster to create and
terminate. terminate.
Communication Inter-Process Communication Inter-Thread Communication
(IPC) is complex and slower is simpler and faster (can
(e.g., pipes, sockets, shared use shared global
memory). variables).
Context Slower, as the OS must Faster.
Switching save/restore a larger state.
Fault Isolation If one process crashes, it If one thread crashes, it can
doesn't affect other processes. take down the entire
process (all other threads).
102. What is Context Switching?
Context switching is the process the operating system (or RTOS scheduler) uses to stop
executing one task (process or thread) and start executing another. This involves:
1. Saving the state (context) of the current task. This includes the program counter, CPU
registers, and memory management information.
2. Loading the state of the new task that is scheduled to run.
3. Resuming execution from the point where the new task was last stopped.
Context switching is essential for multitasking but introduces overhead.
103. What are the types of IPC (Inter-Process Communication) techniques?
IPC techniques are mechanisms that allow different processes to communicate and synchronize
with each other. Common methods include:
● Pipes: A unidirectional communication channel between related processes (e.g., parent
and child).
● FIFO (Named Pipes): A pipe that has a name in the file system, allowing unrelated
processes to communicate.
● Message Queues: A linked list of messages stored in the kernel, where processes can
send and receive messages in a structured way.
● Shared Memory: The fastest IPC method. A block of memory is mapped into the
address space of multiple processes, allowing them to read and write to it directly.
Requires synchronization (e.g., semaphores) to avoid race conditions.
● Semaphores: Used for synchronization between processes to control access to shared
resources and prevent race conditions.
● Sockets: Used for communication between processes on the same machine or across a
network.
104. What is a system call?
A system call is the main interface between a user-level application and the operating system's
kernel. When a program needs a service from the OS (like reading a file, creating a process, or
allocating memory), it executes a system call. This causes a switch from user mode to kernel
mode, where the OS can perform the requested privileged operation safely on behalf of the
application.
Functions like printf() or fopen() are C library functions that internally use system calls (like
write() or open()) to get the job done.
105. Differentiate between Semaphore vs. Mutex vs. Spinlock.
These are all synchronization primitives used to control access to shared resources.
● Mutex (Mutual Exclusion):
○ Purpose: To enforce mutual exclusion and protect a critical section. Only one
thread can own the mutex at a time.
○ Mechanism: If a thread tries to acquire a locked mutex, it gets blocked (put to
sleep) by the OS/scheduler. It yields the CPU until the mutex is released.
○ Ownership: The same thread that locks a mutex must be the one to unlock it.
● Semaphore:
○ Purpose: To control access to a pool of resources or for signaling between
threads. It's essentially a counter.
○ Mechanism: A thread "waits" on the semaphore, which decrements the counter.
If the counter is zero, the thread blocks. A thread "posts" to the semaphore,
which increments the counter and potentially wakes up a blocked thread.
○ Types:
■ Binary Semaphore (value 0 or 1): Can be used like a mutex.
■ Counting Semaphore (any non-negative value): Can be used to allow
a certain number of threads (up to the count) to access a resource
simultaneously.
● Spinlock:
○ Purpose: A very low-level lock for mutual exclusion, typically used inside the OS
kernel or in environments where blocking is not possible (like an ISR).
○ Mechanism: If a thread tries to acquire a locked spinlock, it does not block.
Instead, it enters a tight busy-wait loop ("spins"), repeatedly checking the lock
until it becomes free.
○ Use Case: Only suitable for very short critical sections, as spinning wastes CPU
cycles. Ideal for multi-core systems where the lock-holding thread can run on
another core and release the lock quickly.
Feature Mutex Semaphore Spinlock
Use Case Mutual Exclusion Signaling / Low-level Mutual
Resource Counting Exclusion
Action on Blocks (Sleeps) Blocks (Sleeps) Busy-waits (Spins)
Lock
CPU Usage Low when blocked Low when blocked High when locked
Ownership Yes (lock/unlock by No No
same thread)
Best For Application-level Signaling, Kernel-level
locking, long critical controlling access to locking, very short
sections N resources critical sections
106. Difference between fork() and vfork()?
Both are system calls used to create a new process (a child).
● fork():
○ Creates a new child process that is an exact copy of the parent.
○ It duplicates the parent's entire address space (memory), which can be slow and
resource-intensive (modern systems use copy-on-write to optimize this).
○ Both the parent and child processes can execute concurrently in any order.
● vfork():
○ A legacy, optimized version of fork().
○ It does not create a copy of the parent's memory space. Instead, the child
shares the parent's address space until it calls exec() or _exit().
○ The parent process is suspended (blocked) until the child calls exec() (to load
a new program) or _exit() (to terminate).
○ Danger: Because the child shares the parent's memory, any changes the child
makes (e.g., to variables) will be reflected in the parent, which can be very
dangerous. Its use is now discouraged in favor of posix_spawn().
107. What is a deadlock?
A deadlock is a situation where two or more processes (or threads) are blocked indefinitely,
each waiting for a resource that is held by another process in the set.
For a deadlock to occur, four conditions (known as the Coffman conditions) must be met
simultaneously:
1. Mutual Exclusion: At least one resource must be held in a non-sharable mode. Only
one process can use it at a time.
2. Hold and Wait: A process must be holding at least one resource while waiting to acquire
additional resources held by other processes.
3. No Preemption: Resources cannot be forcibly taken away from a process. They must
be released voluntarily by the process holding them.
4. Circular Wait: A set of processes {P_0,P_1,...,P_n} must exist such that P_0 is waiting
for a resource held by P_1, P_1 is waiting for a resource held by P_2, ..., and P_n is
waiting for a resource held by P_0.
108. What is socket programming? (TCP vs. UDP)
Socket programming is a way of creating applications that communicate over a network. A
socket is an endpoint for sending or receiving data across a network. It acts like a file descriptor
that a program can read from and write to.
The two most common transport protocols used in socket programming are TCP and UDP.
Feature TCP (Transmission Control UDP (User Datagram Protocol)
Protocol)
Connection Connection-oriented. A reliable Connectionless. No
connection must be established connection is established. Data
before data transfer (like a phone is just sent (like a postcard).
call).
Reliability Reliable. Guarantees that data Unreliable. No guarantee of
will arrive in order and without delivery, order, or duplication.
errors (uses acknowledgments "Fire and forget."
and retransmissions).
Speed Slower due to the overhead of Faster due to less overhead.
ensuring reliability (handshakes,
ACKs, flow control).
Header Size Larger header (20 bytes). Smaller header (8 bytes).
Data Flow A stream of bytes. Data is read Datagrams (packets). Data is
as a continuous flow. sent and received in discrete
messages.
Use Case Web Browse (HTTP), file transfer Video/audio streaming, online
(FTP), email (SMTP). Anywhere gaming, DNS. Anywhere
reliability is critical. speed is more important than
perfect reliability.
109. What is the difference between ARM and x86?
This is the primary difference between RISC and CISC architectures.
● ARM:
○ A RISC (Reduced Instruction Set Computer) architecture.
○ Features a simple, small, and highly optimized instruction set.
○ Focuses on low power consumption and efficiency.
○ Dominant in the mobile and embedded systems market (smartphones,
microcontrollers, IoT devices).
● x86:
○ A CISC (Complex Instruction Set Computer) architecture, developed by Intel.
○ Features a large and powerful instruction set, including complex instructions that
can perform multiple operations at once.
○ Focuses on high performance.
○ Dominant in the desktop, laptop, and server market.
110. How can we find the OS and binary architecture?
You can use standard command-line utilities on Linux-based systems.
● To find the OS information:
Use the uname command.
● Bash
# Print all system information
uname -a
# Output might be: Linux my-pc 5.15.0-76-generic #83-Ubuntu SMP ... x86_64 x86_64 x86_64
GNU/Linux
●
●
● To find if a binary file is 32-bit or 64-bit:
Use the file command.
● Bash
# Check an executable file
file /bin/ls
# Output for a 64-bit system:
# /bin/ls: ELF 64-bit LSB pie executable, x86-64, ...
# Output for a 32-bit system would contain "ELF 32-bit"
●
●
111. What is multithreading and why is it used?
Multithreading is a model of execution that allows a single process to have multiple threads of
control running concurrently. These threads share the process's resources but can execute
independently.
Why it is used:
1. Responsiveness: In a user-facing application (like a GUI), one thread can handle user
input while another performs a long-running task in the background, keeping the
application from freezing.
2. Parallelism: On multi-core CPUs, multiple threads can run simultaneously on different
cores, significantly speeding up computationally intensive tasks.
3. Resource Sharing: Threads share memory by default, making it easier and faster to
share data compared to IPC between processes.
4. Efficiency: Creating and managing threads is much faster and less resource-intensive
than creating and managing separate processes.
112. What is GPIO? How to initialize it?
GPIO stands for General-Purpose Input/Output. It refers to the digital pins on a microcontroller
that are not dedicated to a specific function (like UART or SPI) and can be programmatically
controlled by the user's software. They can be used to read the state of a switch, turn an LED
on/off, or bit-bang a simple communication protocol.
How to initialize a GPIO pin (general steps):
The exact register names and bits vary by microcontroller, but the process is always the same.
1. Enable the Clock: The peripheral block for the GPIO port (e.g., GPIOA, GPIOB) must
be powered on by enabling its clock in the clock control register.
2. C
// Example for STM32
RCC->AHB1ENR |= (1 << 0); // Enable clock for GPIOA
3.
4.
5. Set the Direction (Mode): Configure the pin as an input, output, alternate function, or
analog pin by writing to its mode register.
6. C
// Example: Configure pin PA5 as a general-purpose output
GPIOA->MODER &= ~(0x3 << (5 * 2)); // Clear mode bits for pin 5
GPIOA->MODER |= (0x1 << (5 * 2)); // Set mode to 01 (Output)
7.
8.
9. Configure Pin Settings (Optional): Set other properties like output type (push-pull vs.
open-drain), speed, and pull-up/pull-down resistors.
10.C
// Example: Set pin PA5 to push-pull output type (usually the default)
GPIOA->OTYPER &= ~(1 << 5);
11.
12.
After initialization, you can write to the pin using its data register (ODR for output, IDR for input).
113. What is Debouncing?
Debouncing is a technique used to handle the noisy signal produced by a mechanical switch.
When you press or release a physical button, the metal contacts don't make a clean connection
instantly. They "bounce" against each other rapidly for a few milliseconds, creating a series of
fast on/off electrical pulses.
If you read the pin's state directly, the microcontroller would see this bouncing as multiple quick
presses. Debouncing is the process of filtering out these spurious pulses to ensure that only a
single, stable key press event is registered.
This can be done in software (by reading the pin, waiting a small delay, and reading it again to
see if the state is stable) or in hardware (using an RC filter).
114. Explain CAN frame and different errors.
CAN (Controller Area Network) is a robust vehicle bus standard designed to allow
microcontrollers and devices to communicate with each other's applications without a host
computer.
A CAN frame is the packet of information transmitted on the CAN bus. The most common is the
standard data frame, which includes:
● Arbitration Field: Contains the message Identifier (ID) and a bit that determines if it's a
data or remote frame. The ID also sets the message's priority (lower ID = higher
priority).
● Control Field: Specifies the number of data bytes.
● Data Field: Contains the actual data (0 to 8 bytes).
● CRC Field: A cyclic redundancy check for error detection.
● ACK Field: A slot where receiving nodes can acknowledge successful receipt of the
frame.
● End of Frame: Marks the end of the CAN frame.
Different Errors in CAN Communication:
CAN has a sophisticated fault confinement mechanism.
1. Bit Error: A transmitter monitors the bus while sending. If it sends a bit but detects a
different level on the bus, it signals a bit error.
2. Stuff Error: CAN uses bit stuffing (inserting an opposite bit after five consecutive
identical bits) to ensure enough signal edges for synchronization. A stuff error occurs if
six consecutive identical bits are detected.
3. CRC Error: A receiver calculates the CRC of the message and compares it to the CRC
in the frame. If they don't match, it signals a CRC error.
4. Form Error: Occurs if a fixed-format field (like the End of Frame) contains an illegal
value.
5. Acknowledgment (ACK) Error: Occurs if the transmitter does not receive an ACK from
any receiver.
115. What is a Bootloader?
A bootloader is a small, special-purpose program that runs when a system is first powered on.
Its primary job is to initialize the hardware to a minimal state and then load the main application
or operating system from storage (like Flash memory or a hard drive) into the main memory
(RAM) and start its execution.
In embedded systems, a bootloader is often used to enable in-field firmware updates, typically
over a communication interface like UART, USB, or Ethernet.
116. Write a code showing the pthread prototype and how to find if a
number is odd/even in a thread.
C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// The function that the thread will execute.
// It must have a 'void*' return type and take a 'void*' argument.
void* check_odd_even(void* arg) {
// Cast the void pointer argument back to the correct type.
int number = *(int*)arg;
if (number % 2 == 0) {
printf("Thread says: %d is Even.\n", number);
} else {
printf("Thread says: %d is Odd.\n", number);
}
// A thread function should return a void pointer.
// NULL is a common return value if no specific data needs to be returned.
return NULL;
}
int main() {
pthread_t thread_id; // Variable to hold the thread identifier.
int num_to_check = 25;
printf("Main says: Creating a thread to check the number %d.\n", num_to_check);
// Create the thread.
// 1st arg: Pointer to the pthread_t variable.
// 2nd arg: Attributes for the thread (NULL for default).
// 3rd arg: The function the thread will run.
// 4th arg: The argument to pass to the thread function.
if (pthread_create(&thread_id, NULL, check_odd_even, &num_to_check) != 0) {
perror("Failed to create thread");
return 1;
}
// Wait for the thread to finish its execution.
// This is important to ensure the main program doesn't exit before the thread is done.
if (pthread_join(thread_id, NULL) != 0) {
perror("Failed to join thread");
return 1;
}
printf("Main says: Thread has finished.\n");
return 0;
}
117. Write a function pointer callback example.
This example shows a do_operation function that takes two numbers and a function pointer (a
callback) to perform an operation on them.
#include <stdio.h>
// Define a function pointer type for clarity
typedef int (*operation_t)(int, int);
// Callback function 1: add
int add(int a, int b) {
printf("---Executing add() callback---\n");
return a + b;
}
// Callback function 2: subtract
int subtract(int a, int b) {
printf("---Executing subtract() callback---\n");
return a - b;
}
// This function takes a callback to perform a task
void do_operation(int x, int y, operation_t op_func) {
int result = op_func(x, y); // "Calling back" the function
printf("Result of operation: %d\n", result);
}
// This function demonstrates invoking a function (sub) from another (add)
// via a callback passed as an argument.
void add_and_call_another(int a, int b, operation_t another_op) {
int add_result = a + b;
printf("Inside add_and_call_another, add result is: %d\n", add_result);
printf("Now invoking the passed callback (subtract)...\n");
do_operation(a, b, another_op); // Invoke subtract from within add
}
int main() {
printf("--- Example 1: Basic Callback ---\n");
do_operation(10, 5, add); // Pass 'add' as the callback
do_operation(10, 5, subtract); // Pass 'subtract' as the callback
printf("\n--- Example 2: Invoking sub from add via callback ---\n");
add_and_call_another(20, 8, subtract);
return 0;
}
118. What is the difference between *p++, ++*p, and (*p)++?
This question tests your understanding of operator precedence and side effects. Let's use an
array to see the difference.
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30};
int *p = arr;
// Case 1: *p++ (Value, then increment pointer)
// Postfix ++ has higher precedence than *, but is evaluated after the expression.
// 1. Gets the value at the current address (*p which is 10).
// 2. Increments the pointer p to point to the next element (arr[1]).
printf("Value of *p++ is: %d\n", *p++);
printf("After *p++, p now points to: %d\n\n", *p); // p now points to 20
// Reset pointer for next example
p = arr;
// Case 2: ++*p (Increment value)
// Prefix ++ and * have same precedence, evaluated right-to-left.
// 1. Dereferences p (*p gets the value 10).
// 2. Increments that value (++10 becomes 11). The original array is modified.
// The pointer p does NOT move.
printf("Value of ++*p is: %d\n", ++*p);
printf("After ++*p, p still points to: %d\n", *p); // p still points to arr[0], which is now 11
printf("Array is now: {%d, %d, %d}\n\n", arr[0], arr[1], arr[2]);
// Reset pointer for next example
p = arr;
arr[0] = 10; // Reset value
// Case 3: (*p)++ (Value, then increment value)
// Parentheses force dereferencing first.
// 1. Dereferences p (*p gets the value 10).
// 2. The expression evaluates to this value (10).
// 3. The value in memory is then incremented as a side effect (arr[0] becomes 11).
// The pointer p does NOT move.
printf("Value of (*p)++ is: %d\n", (*p)++);
printf("After (*p)++, p still points to: %d\n", *p); // p still points to arr[0], which is now 11
printf("Array is now: {%d, %d, %d}\n", arr[0], arr[1], arr[2]);
return 0;
}
Summary:
● *p++: Returns the value p points to, then increments the pointer p.
● ++*p: Increments the value p points to, then returns the new incremented value.
● (*p)++: Returns the value p points to, then increments the value in memory.
119. What happens during compilation if I use a static function?
When you declare a function as static, you are giving it internal linkage.
This means the function's symbol (its name) is not made visible to the linker for other object
files to use. The function is private to the .c file in which it is defined. If you try to call that static
function from another file (using an extern declaration), the linker will fail with an "undefined
reference" error because it cannot find the function's symbol in its global symbol table.
120. Linear vs. Non-Linear Data Structures
● Linear Data Structures: Elements are arranged in a sequential or linear order. Each
element is connected to its previous and next element. Traversal is sequential.
○ Examples: Arrays, Linked Lists, Stacks, Queues.
● Non-Linear Data Structures: Elements are not arranged sequentially. An element can
be connected to multiple other elements, forming a hierarchical or network structure.
○ Examples: Trees (e.g., Binary Trees, Heaps), Graphs.
121. Write code to find the second highest digit in a number.
C
#include <stdio.h>
int find_second_largest_digit(int n) {
int largest = -1;
int second_largest = -1;
if (n < 0) n = -n; // Handle negative numbers
while (n > 0) {
int digit = n % 10;
if (digit > largest) {
second_largest = largest;
largest = digit;
} else if (digit > second_largest && digit < largest) {
second_largest = digit;
}
n /= 10;
}
return second_largest;
}
int main() {
int num1 = 59281;
int num2 = 1234;
int num3 = 998;
int num4 = 5;
printf("Second largest digit in %d is %d\n", num1, find_second_largest_digit(num1)); // Output: 8
printf("Second largest digit in %d is %d\n", num2, find_second_largest_digit(num2)); // Output: 3
printf("Second largest digit in %d is %d\n", num3, find_second_largest_digit(num3)); // Output: 8
printf("Second largest digit in %d is %d\n", num4, find_second_largest_digit(num4)); // Output: -1
(no second largest)
return 0;
}
122. Typecasting vs. Type Conversion
Although often used interchangeably, there's a subtle difference.
● Type Conversion (Implicit): This is done automatically by the compiler when you mix
data types in an expression. The compiler converts the "lower" type to the "higher" type
to avoid data loss before performing the operation. This is also known as "type
promotion."
● C
int i = 10;
float f = 3.5;
float result = i + f; // 'i' is implicitly converted to a float (10.0f) before the addition.
●
●
● Type Casting (Explicit): This is done manually by the programmer using the cast
operator (type). You are explicitly telling the compiler to convert one data type to another,
even if it might lead to data loss.
● C
double d = 9.87;
int i = (int)d; // 'd' is explicitly cast to an int. The fractional part is truncated. 'i' becomes 9.
●
●
123. Difference between inline vs. macro.
Both are used to avoid function call overhead, but they are fundamentally different.
Feature inline Function #define Macro
Processor Handled by the compiler. Handled by the preprocessor.
Type Yes. The compiler checks No. It's just blind text replacement,
Checking the types of arguments. which can lead to bugs.
Debugging Easy. The function name Hard. The macro doesn't exist at
exists for the debugger, compile time, so it's invisible to the
and you can step through debugger.
it.
Argument Arguments are evaluated Arguments may be evaluated
Evaluation once before the function is multiple times, leading to
called. unexpected side effects (e.g.,
MAX(a++, b++)).
Nature It's a true function with its It's a text token with no concept of
own scope. scope.
Compiler's The inline keyword is only The preprocessor must expand the
Role a request. The compiler macro.
can choose to ignore it.
Conclusion: Prefer inline functions over function-like macros for type safety and easier
debugging. Use macros for simple constant definitions.
124. Explain Interrupts, Interrupt Vector Table (IVT), and ISR.
These three components work together to handle hardware events efficiently.
1. Interrupt: An asynchronous signal from hardware (or software) that tells the CPU to stop
its current task and handle a more urgent event. Examples: a timer overflow, a button
press, data arriving over UART.
2. Interrupt Vector Table (IVT): A special table stored at a fixed location in memory. It's
essentially an array of pointers. Each entry (or "vector") in the table corresponds to a
specific interrupt source and holds the memory address of the function that should be
executed for that interrupt.
3. Interrupt Service Routine (ISR): The actual function whose address is stored in the
IVT. When a specific interrupt occurs, the CPU looks up the corresponding address in
the IVT and jumps to that ISR to execute it.
The process:
Hardware Event -> Interrupt Signal to CPU -> CPU uses IVT to find ISR address -> CPU
executes the ISR -> CPU resumes its original task.
125. Can an interrupt exist without an ISR?
Yes, an interrupt can occur, but what happens next depends on the system. The hardware will
still generate the interrupt signal and the CPU will stop and check the Interrupt Vector Table.
If the entry in the IVT for that interrupt has not been populated with the address of a valid ISR,
one of two things usually happens:
1. The vector points to a default, generic fault handler. This handler might put the system
into a safe state, log an error, or simply enter an infinite loop, indicating an unhandled
interrupt has occurred.
2. The vector contains a NULL or garbage value, which will likely cause the CPU to crash
when it tries to jump to an invalid address, resulting in a hard fault.
So, while the interrupt event can exist, it cannot be handled meaningfully without a
corresponding ISR.
126. How are multiple pending interrupts handled?
This is managed by the Interrupt Controller (like the NVIC in ARM Cortex-M).
1. Prioritization: Each interrupt source is assigned a priority level.
2. Pending State: When an interrupt occurs, the controller marks it as "pending." If the
CPU is busy or handling another interrupt, the new interrupt waits in this pending state.
3. Arbitration: When the CPU is ready to handle an interrupt (e.g., it just finished an ISR),
the controller checks all pending interrupts and selects the one with the highest priority
to be serviced next.
4. Preemption (Nested Interrupts): If a low-priority ISR is currently executing and a new,
higher-priority interrupt occurs, the controller will cause the CPU to pause the low-priority
ISR and immediately start executing the high-priority one. The lower-priority ISR will
resume only after the higher-priority one is complete.
127. What is a device driver?
A device driver is a specific type of software program that acts as a translator or interface
between the operating system (or a user application) and a particular hardware device.
Its purpose is to abstract the hardware's complexity. The application programmer doesn't
need to know the low-level details of how to control a device (e.g., which registers to write to).
Instead, they can use simple, high-level function calls provided by the driver (like uart_send(),
disk_read()), and the driver handles the underlying hardware interaction.
128. What is a timer? How to initialize a timer?
A timer is a fundamental hardware peripheral in a microcontroller. It's a counter that can be
configured to increment (or decrement) at a specific frequency, usually derived from the system
clock.
Uses:
● Generating precise time delays.
● Creating periodic interrupts to run tasks at regular intervals (e.g., blinking an LED every
500ms).
● Measuring the duration of events.
● Generating PWM (Pulse Width Modulation) signals to control motors or dim LEDs.
How to initialize a timer (general steps):
1. Enable the Clock: Power on the timer peripheral by enabling its clock in a clock control
register.
2. Configure Prescaler: The system clock is often too fast. The prescaler divides the
system clock to produce a slower, more usable timer clock frequency. Timer_Freq =
System_Clock / (Prescaler + 1).
3. Set Auto-Reload Value: The auto-reload register (ARR) holds the value the timer will
count up to before it "overflows" and resets to zero. This determines the period of the
timer. Period = (ARR + 1) / Timer_Freq.
4. Enable Interrupts (Optional): Configure the timer to generate an interrupt on overflow
(an "update event").
5. Enable the Timer: Set a bit in a control register to start the counter.
129. Write a code to count the total number of "to" in a string.
C
#include <stdio.h>
#include <string.h>
int count_substring(const char *str, const char *sub) {
if (!str || !sub) return 0;
int count = 0;
const char *p = str;
// strstr finds the first occurrence of sub in p
while ((p = strstr(p, sub)) != NULL) {
count++;
// Move pointer past the found substring to avoid infinite loops
p += strlen(sub);
}
return count;
}
int main() {
const char *my_string = "Welcome to gdb Welcome to gdb Welcome to gdb";
int num_found = count_substring(my_string, "to");
printf("The substring 'to' was found %d times.\n", num_found); // Output: 3
return 0;
}
130. Static vs. Shared Libraries
Libraries are collections of pre-compiled code (functions, variables) that can be reused by
multiple programs.
● Static Library (.a or .lib):
○ The code from the library is copied directly into your final executable file by the
linker during compilation.
○ Pros: The executable is self-contained and doesn't depend on external files to
run.
○ Cons: Increases the size of every executable that uses it. If the library is
updated, you must recompile your entire program to include the changes.
● Shared Library (or Dynamic Library, .so or .dll):
○ The library code is not copied into your executable. Instead, the executable
contains only a reference to the library.
○ The library is loaded into memory by the operating system just once at runtime
and can be shared by multiple programs simultaneously.
○ Pros: Smaller executable files. Saves memory, as the library is only loaded once.
You can update the library without having to recompile your programs.
○ Cons: The program depends on the shared library file being present on the
target system.
131. What is Memory Corruption?
Memory corruption is a type of error where a program accidentally modifies the content of a
memory location that it shouldn't. This can happen due to various bugs and leads to
unpredictable behavior, including incorrect data, crashes (like segmentation faults), or security
vulnerabilities.
Common Causes:
● Buffer Overflows: Writing past the allocated boundary of an array or buffer.
● Using Dangling Pointers: Writing to memory that has already been freed.
● Using Uninitialized (Wild) Pointers: Writing to a random memory address.
● Incorrect Type Casting: Forcing the interpretation of data as a different, incompatible
type.
132. Implement common string library functions.
Here are simple implementations of strlen, strcpy, strcat, and strcmp.
#include <stddef.h> // For size_t
// --- strlen: returns the length of a string ---
size_t my_strlen(const char *str) {
size_t len = 0;
while (str[len] != '\0') {
len++;
}
return len;
}
// --- strcpy: copies a string ---
char* my_strcpy(char *dest, const char *src) {
char *original_dest = dest;
while ((*dest++ = *src++) != '\0');
return original_dest;
}
// --- strcat: concatenates (appends) a string ---
char* my_strcat(char *dest, const char *src) {
char *original_dest = dest;
// Move dest pointer to the end of the destination string
while (*dest != '\0') {
dest++;
}
// Copy the source string to the end
while ((*dest++ = *src++) != '\0');
return original_dest;
}
// --- strcmp: compares two strings ---
int my_strcmp(const char *s1, const char *s2) {
while (*s1 && (*s1 == *s2)) {
s1++;
s2++;
}
// Return the difference of the first non-matching characters
return *(const unsigned char*)s1 - *(const unsigned char*)s2;
}
133. Linked List: Reverse and Find Nth Node
Reverse a Linked List (Iterative):
This is a classic pointer manipulation problem. You need three pointers: prev, current, and next.
void reverse_list(Node_t **head) {
Node_t *prev = NULL;
Node_t *current = *head;
Node_t *next_node = NULL;
while (current != NULL) {
next_node = current->next; // Store the next node
current->next = prev; // Reverse the current node's pointer
prev = current; // Move prev one step forward
current = next_node; // Move current one step forward
}
*head = prev; // The new head is the old last node
}
Find the Nth Node from the End:
The efficient way is to use two pointers.
1. Move a fast pointer N nodes ahead.
2. Then, start a slow pointer from the head.
3. Move both fast and slow pointers one step at a time until fast reaches the end of the list.
The slow pointer will then be at the Nth node from the end.
134. What if calloc cannot find contiguous memory?
If calloc() (or malloc()) cannot find a single, contiguous block of memory of the requested size
on the heap, it will fail. When it fails, it returns a NULL pointer. It will not try to allocate a
non-contiguous block. This is why you must always check the pointer returned from memory
allocation functions before using it.
int *arr = (int*)calloc(HUGE_NUMBER, sizeof(int));
if (arr == NULL) {
// Allocation failed. Handle the error.
printf("Failed to allocate memory!\n");
return 1;
}
135. What are /proc and /dev in Linux?
These are special virtual filesystems in Linux that provide interfaces to the kernel and hardware.
● /proc (Process Filesystem):
○ A virtual filesystem that provides a window into the kernel's runtime
information. It doesn't contain real files on disk.
○ It's mainly used to access information about running processes. Each running
process has a directory named after its process ID (e.g., /proc/1234).
○ It also contains files with system information like /proc/cpuinfo (CPU details) and
/proc/meminfo (memory usage).
● /dev (Device Filesystem):
○ Contains special files called device files, which represent and provide an
interface to the hardware devices connected to the system (both physical and
virtual).
○ It allows user programs to interact with hardware as if they were regular files
(e.g., you can read from /dev/random or write to /dev/ttyS0 for a serial port).
136. What is the Base Condition in recursion?
The base condition (or base case) is the condition in a recursive function that stops the
recursion. It's a simple case that can be solved directly without making another recursive call.
Without a base condition, a recursive function would call itself forever, leading to infinite
recursion and a stack overflow.
// Example: Factorial function
int factorial(int n) {
// Base Condition: Stops recursion when n is 1
if (n <= 1) {
return 1;
}
// Recursive Step
return n * factorial(n - 1);
}
137. What are Handshake Signals in UART?
Standard UART communication (using TX and RX lines) has no flow control, which can be a
problem if the receiver can't process data as fast as the sender is transmitting it. Hardware
handshaking adds two extra signal lines to manage this data flow.
● RTS (Request to Send): A signal from the sender to the receiver. The sender asserts
this line (e.g., sets it low) when it wants to send data.
● CTS (Clear to Send): A signal from the receiver back to the sender. The receiver
asserts this line when it is ready to accept data.
How it works:
1. A sender wants to transmit. It asserts its RTS line.
2. It then waits for the receiver to assert the CTS line.
3. Once CTS is asserted, the sender begins transmitting data.
4. If the receiver's buffer is about to get full, it will de-assert its CTS line, telling the sender
to pause transmission.
138. How is interrupt latency measured?
Measuring interrupt latency precisely requires observing the hardware signals. The two common
methods are:
1. Using an Oscilloscope or Logic Analyzer: This is the most accurate method.
○ In your code, configure a GPIO pin to toggle (e.g., go from low to high) at the
very beginning of your ISR.
○ Connect the logic analyzer probe to this GPIO pin and to the hardware line that
generates the interrupt signal.
○ The time difference measured on the analyzer between the interrupt signal firing
and the GPIO pin changing state is your interrupt latency.
2. Using a High-Resolution Timer:
○ Configure a free-running hardware timer with a high frequency.
○ In the ISR, the very first action is to read the timer's current count value.
○ This gives you a timestamp of when the ISR started. If you also have a
timestamp of when the interrupt occurred (which some peripherals provide), the
difference is the latency.
139. SPI vs. I2C vs. UART: Synchronous or Asynchronous?
This is about whether a separate clock signal is used to synchronize data transfer.
● Synchronous Communication: Uses a dedicated clock line shared between the
sender and receiver. The data is transferred on the edges of this clock signal.
○ SPI: Synchronous. It has a dedicated clock line (SCLK).
○ I2C: Synchronous. It has a dedicated clock line (SCL).
● Asynchronous Communication: Does not use a separate clock line. Instead, the
sender and receiver must agree on a timing parameter (the Baud Rate) beforehand. The
timing is synchronized for each byte using start and stop bits.
○ UART: Asynchronous. It only uses TX and RX lines for data.
○ CAN: This one is unique. It is asynchronous in that there is no master clock
line. However, the nodes on the bus actively synchronize with each other using
the bit transitions in the data frames (a technique called bit synchronization). So
it's often called "asynchronous but self-synchronizing".
140. What are common architectures in Embedded Systems?
Besides ARM, which is the most dominant, several other architectures are widely used:
● ARM: The industry standard for everything from low-power microcontrollers (Cortex-M)
to high-performance application processors (Cortex-A).
● RISC-V: A modern, open-standard (royalty-free) RISC architecture that is gaining
significant traction as a flexible alternative to ARM.
● AVR: A modified Harvard architecture 8-bit RISC architecture, famous for its use in
Atmel (now Microchip) microcontrollers, including those used in Arduino boards.
● PIC (Peripheral Interface Controller): A family of microcontrollers from Microchip,
known for their robustness and wide use in industrial and hobbyist applications.
● MIPS (Microprocessor without Interlocked Pipelined Stages): A RISC architecture
that was once very popular in networking equipment and embedded systems. Its
influence is waning but still present.
● 8051: A very old but still popular 8-bit architecture originally developed by Intel. It's
simple, cheap, and used in many low-cost applications.
141. Explain SPI CPOL and CPHA.
CPOL (Clock Polarity) and CPHA (Clock Phase) are two configuration bits that define the
timing relationship between the clock and data in an SPI communication. The master and slave
must be configured with the same CPOL/CPHA mode to communicate correctly. There are four
possible modes.
● CPOL (Clock Polarity): Defines the idle state of the clock line (SCLK).
○ CPOL=0: The clock is idle low. The active state is high.
○ CPOL=1: The clock is idle high. The active state is low.
● CPHA (Clock Phase): Defines on which clock edge the data is sampled (read).
○ CPHA=0: Data is sampled on the first clock edge of the cycle (the one that
transitions from idle to active).
○ CPHA=1: Data is sampled on the second clock edge of the cycle (the one that
transitions from active back to idle).
Mode CPOL CPHA Description
0 0 0 Clock idle low, data sampled on rising edge.
1 0 1 Clock idle low, data sampled on falling edge.
2 1 0 Clock idle high, data sampled on falling edge.
3 1 1 Clock idle high, data sampled on rising edge.
Best of luck with your interviews!