1. Simulate a simple arithmetic operation (e.g.
, addition, subtraction) in both a CISC-like and
RISC-like manner. The CISC simulation should perform the operation in a single step, while
the RISC simulation should break it down into simpler steps
2. Design a program that translates a small set of assembly-like instructions (define your simple
instruction set) into a simulated machine code. Your program should handle basic operations
like load, store, add, and subtract
3. Design a program that simulates the basic functions of a linker and loader for a simplified
computational system
4. Implement a simple macro processor that allows for the definition and expansion of macros
within a text file. The macros should perform simple text replacement or predefined
operations (like incrementing a number)
5. Design and implement a simple arithmetic expression evaluator using YACC. The evaluator
should be capable of handling basic arithmetic operations (+, -, *, /) and correctly respects
the standard mathematical precedence of operations and handles parentheses to alter the
precedence order.
Problem 1:
To structure C code in a way that reflects the philosophies of CISC (Complex Instruction Set
Computer) and RISC (Reduced Instruction Set Computer), you can focus on how operations are
grouped and handled, the types of instructions used, and how memory is accessed. Although C is a
high-level language, you can simulate the design philosophies of CISC and RISC by paying attention to
the following aspects:
1. Instruction Complexity:
CISC Philosophy: Combine multiple operations in a single complex statement.
RISC Philosophy: Break down operations into simpler, more granular steps.
2. Memory Access:
CISC Philosophy: Perform operations directly on memory with fewer load/store operations
(e.g., using more complex instructions that might implicitly handle memory access).
RISC Philosophy: Separate data movement from processing by using explicit load/store
instructions (i.e., operations should work only on registers).
3. Optimization for Hardware:
CISC Philosophy: Optimize for fewer instructions (even if they are complex).
RISC Philosophy: Optimize for simpler instructions executed faster, even if more instructions
are needed.
Structuring C Code for CISC Philosophy:
In the CISC style, you might write more complex, high-level expressions that involve multiple
operations and memory accesses, with fewer explicit load/store operations. You rely on the compiler
or hardware to optimize the execution of complex statements.
c
Copy code
#include <stdio.h>
int main() {
int array[5] = {1, 2, 3, 4, 5};
int sum = 0;
// CISC-like approach: Complex expression with multiple operations and memory access in one
step
sum = (array[0] + array[1]) * (array[2] - array[3]) / array[4];
printf("CISC-like result: %d\n", sum);
return 0;
Key CISC-like aspects in this code:
1. Complex Arithmetic: Multiple arithmetic operations (addition, subtraction, multiplication,
division) are combined in a single expression.
2. Implicit Memory Access: The code accesses memory directly when working with the array,
without separating load/store explicitly.
3. Fewer Instructions (at high-level): Fewer lines of code that handle multiple operations in a
single expression.
Structuring C Code for RISC Philosophy:
For RISC, you should break down complex operations into smaller, simpler steps. Each instruction
performs one operation, and load/store instructions are separated from computational operations.
Copy code
#include <stdio.h>
int main() {
int array[5] = {1, 2, 3, 4, 5};
int sum = 0;
int temp1, temp2, temp3;
// RISC-like approach: Simple operations, explicit load/store instructions
temp1 = array[0] + array[1]; // Addition
temp2 = array[2] - array[3]; // Subtraction
temp3 = temp1 * temp2; // Multiplication
sum = temp3 / array[4]; // Division
printf("RISC-like result: %d\n", sum);
return 0;
Key RISC-like aspects in this code:
1. Simple Instructions: Each arithmetic operation is performed in its own line of code.
2. Explicit Load/Store: Memory access is separated, with clear stages (load the values from
array into temporary variables, perform arithmetic, and then store the result).
3. More Instructions (at high-level): The code has more lines, but each line performs a simple,
single operation.
4. Register-based: While C doesn't use registers explicitly, you can think of the variables
(temp1, temp2, etc.) as registers in a CPU, in line with RISC's load/store architecture.
Further Aspects of CISC vs RISC Philosophy in C Code:
1. Loop Optimization:
o In CISC, loops might involve complex condition checks and operations, potentially
involving memory at each step.
o In RISC, you break down loop conditions into separate checks and focus on
processing data in registers, with memory access limited to loading and storing.
Example:
Copy code
// CISC-like loop (complex loop condition and operation)
for (int i = 0; i< 5 && (array[i] = array[i] * 2) != 0; i++) {
// Some operations
}
// RISC-like loop (simplified)
for (int i = 0; i< 5; i++) {
array[i] = array[i] * 2; // Single operation per iteration
2. Function Calls:
o CISC: A function may handle many operations internally, with the function signature
appearing complex but doing a lot of work.
o RISC: Break down complex functions into smaller, more focused functions with each
function handling a specific task.
Copy code
// CISC-like function
int calculate(int a, int b, int c, int d) {
return (a + b) * (c - d) / 2; // Complex expression
// RISC-like approach with smaller functions
int add(int a, int b) {
return a + b;
int subtract(int a, int b) {
return a - b;
int multiply(int a, int b) {
return a * b;
int calculate(int a, int b, int c, int d) {
int sum = add(a, b);
int diff = subtract(c, d);
return multiply(sum, diff) / 2; // Simpler, broken down
Summary of Key Differences:
CISC-style code tends to use complex, higher-level expressions that often hide multiple
operations within single statements. It may perform implicit memory operations and
combines tasks into fewer lines of code.
RISC-style code emphasizes simplicity and separation, where each operation (add, subtract,
multiply, load/store) is handl
ed separately and explicitly, reflecting RISC's design of simple instructions that execute in a
single clock cycle.
By adopting these principles in C code structure, you can simulate the different philosophies of CISC
and RISC at the software level.
Problem 2:
C Program to Translate Assembly-like Code to Machine Code:
Copy code
#include <stdio.h>
#include <string.h>
// Define instruction opcodes
#define LOAD_OPCODE 0b0001
#define STORE_OPCODE 0b0010
#define ADD_OPCODE 0b0011
#define SUB_OPCODE 0b0100
// Function to convert assembly-like instruction to machine code
int translateInstruction(char *instruction, int *machineCode) {
char instr[10], arg1[10], arg2[10];
int opcode = 0, reg1 = 0, reg2 = 0;
// Parse the instruction (e.g., LOAD R1, M2)
sscanf(instruction, "%s %s %s", instr, arg1, arg2);
// Remove commas if present
for (int i = 0; arg1[i] != '\0'; i++) {
if (arg1[i] == ',') arg1[i] = '\0';
// Determine the opcode and operands
if (strcmp(instr, "LOAD") == 0) {
opcode = LOAD_OPCODE;
reg1 = arg1[1] - '0'; // Extract register number
reg2 = arg2[1] - '0'; // Extract memory number
else if (strcmp(instr, "STORE") == 0) {
opcode = STORE_OPCODE;
reg1 = arg1[1] - '0'; // Extract register number
reg2 = arg2[1] - '0'; // Extract memory number
else if (strcmp(instr, "ADD") == 0) {
opcode = ADD_OPCODE;
reg1 = arg1[1] - '0'; // Extract first register
reg2 = arg2[1] - '0'; // Extract second register
else if (strcmp(instr, "SUB") == 0) {
opcode = SUB_OPCODE;
reg1 = arg1[1] - '0';
reg2 = arg2[1] - '0';
else {
printf("Unknown instruction: %s\n", instr);
return -1;
// Construct machine code: 4-bit opcode + 2-bit reg1 + 2-bit reg2/memory
*machineCode = (opcode << 4) | (reg1 << 2) | reg2;
return 0;
// Function to print machine code in binary and hexadecimal format
void printMachineCode(int machineCode) {
printf("Machine Code (binary): ");
for (int i = 7; i>= 0; i--) {
printf("%d", (machineCode>>i) & 1);
printf("\nMachine Code (hex): 0x%02X\n", machineCode);
int main() {
char instruction[20];
int machineCode = 0;
// Input assembly-like instruction
printf("Enter an instruction (e.g., LOAD R1, M2): ");
fgets(instruction, sizeof(instruction), stdin);
// Translate the instruction to machine code
if (translateInstruction(instruction, &machineCode) == 0) {
printMachineCode(machineCode);
return 0;
Problem 3:
Program Structure
1. Object File Simulation:
o Each object file contains:
Code (instructions like LOAD, STORE, ADD, etc.).
Symbol Table (function and variable references with addresses).
2. Linker:
o Combines multiple object files into a single executable.
o Resolves external symbols (i.e., function or variable calls across files).
o Adjusts the memory addresses of the code and data in each object file.
3. Loader:
o Loads the linked executable into memory.
o Simulates execution by interpreting the instructions.
Simplified Assumptions:
Each object file contains simple operations (LOAD, STORE, ADD, etc.).
The memory model is a simple array where each cell can hold an instruction or data.
The program resolves symbols (variables/functions) using a basic symbol table.
Example Program in C
Here’s a basic simulation of a linker and loader:
Copy code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_MEMORY 1024
#define MAX_SYMBOLS 100
#define MAX_CODE 256
// Instruction types for simplicity
typedef enum { LOAD, STORE, ADD, SUB, HALT } InstructionType;
// A symbol representing a function or variable
typedef struct {
char name[32];
int address;
} Symbol;
// A structure representing an object file (code + symbols)
typedef struct {
char *code[MAX_CODE];
Symbol symbols[MAX_SYMBOLS];
int code_size;
int symbol_count;
} ObjectFile;
// A structure representing the final linked executable
typedef struct {
char *code[MAX_MEMORY];
int code_size;
} Executable;
// Memory model (for loading and execution)
char *memory[MAX_MEMORY];
// Linker: combines object files, resolves symbols, adjusts addresses
void linker(ObjectFileobjectFiles[], int numFiles, Executable *exec) {
int currentAddress = 0; // Memory address counter
for (int i = 0; i<numFiles; i++) {
ObjectFile *obj = &objectFiles[i];
// Adjust code addresses and copy code to the executable
for (int j = 0; j <obj->code_size; j++) {
exec->code[currentAddress++] = obj->code[j];
exec->code_size = currentAddress;
// Simple symbol resolution (you can expand this to handle external references)
// For now, we assume no unresolved symbols between object files
printf("Linking completed. Final code size: %d instructions\n", exec->code_size);
// Loader: loads the executable into memory
void loader(Executable *exec) {
for (int i = 0; i< exec->code_size; i++) {
memory[i] = exec->code[i];
printf("Loading completed. Code loaded into memory.\n");
// Simple instruction execution simulation
void execute() {
int pc = 0; // Program counter
int acc = 0; // Accumulator (for simplicity)
while (1) {
char *instruction = memory[pc];
if (strcmp(instruction, "LOAD") == 0) {
acc = 10; // Just an example, you can modify this
printf("Executing LOAD. Accumulator = %d\n", acc);
} else if (strcmp(instruction, "STORE") == 0) {
printf("Executing STORE. Value stored = %d\n", acc);
} else if (strcmp(instruction, "ADD") == 0) {
acc += 5; // Another example
printf("Executing ADD. Accumulator = %d\n", acc);
} else if (strcmp(instruction, "HALT") == 0) {
printf("Execution halted.\n");
break;
pc++;
int main() {
// Object files simulation
ObjectFile obj1 = {
.code = {"LOAD", "ADD", "STORE", "HALT"},
.code_size = 4,
.symbol_count = 0
};
ObjectFile obj2 = {
.code = {"LOAD", "STORE", "HALT"},
.code_size = 3,
.symbol_count = 0
};
// Array of object files
ObjectFile objectFiles[] = { obj1, obj2 };
// Executable to hold linked code
Executable exec;
exec.code_size = 0;
// Linking object files
linker(objectFiles, 2, &exec);
// Loading executable into memory
loader(&exec);
// Execute the loaded program
execute();
return 0;
Explanation
1. Object Files (ObjectFile):
o Each object file contains a series of instructions (code[]) and a symbol table
(symbols[]).
o code_size holds the number of instructions.
2. Linker:
o The linker() function combines the instructions of all object files into a single
executable (Executable).
o It assumes all symbols are resolved within individual object files, though this can be
expanded to handle cross-file references.
3. Loader:
o The loader() function loads the final linked executable into a memory array
(memory[]).
4. Execution:
o The execute() function simulates a simple instruction execution cycle, where each
instruction is interpreted, and the program halts on encountering a HALT instruction.
Problem 4:
Program Design:
Input: A text file that may contain macro definitions and regular text.
Output: The processed text with macros expanded.
Steps:
1. Read the input file.
2. Parse macro definitions (lines starting with #define).
3. Store macros in a symbol table (using a simple array).
4. Process the rest of the text, expanding any macros found by looking them up in the symbol
table.
C Program
Copy code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_MACROS 100 // Maximum number of macros
#define MAX_LINE_LENGTH 256 // Maximum length of each line
#define MAX_MACRO_NAME 32 // Maximum macro name length
#define MAX_REPLACEMENT 256 // Maximum replacement text length
// Macro structure to store macro definitions
typedef struct {
char name[MAX_MACRO_NAME];
char replacement[MAX_REPLACEMENT];
} Macro;
// Array to hold all macro definitions
Macro macros[MAX_MACROS];
int macro_count = 0;
// Function to define a macro
void define_macro(char *name, char *replacement) {
if (macro_count>= MAX_MACROS) {
printf("Error: Too many macros defined.\n");
return;
strcpy(macros[macro_count].name, name);
strcpy(macros[macro_count].replacement, replacement);
macro_count++;
// Function to find a macro by name
const char *find_macro_replacement(const char *name) {
for (int i = 0; i<macro_count; i++) {
if (strcmp(macros[i].name, name) == 0) {
return macros[i].replacement;
return NULL; // Return NULL if macro not found
// Function to process each line, expanding macros
void process_line(char *line) {
char processed_line[MAX_LINE_LENGTH] = "";
char *token = strtok(line, " \t\n");
while (token != NULL) {
const char *replacement = find_macro_replacement(token);
if (replacement != NULL) {
strcat(processed_line, replacement); // Use the macro replacement
} else {
strcat(processed_line, token); // No replacement, keep the original
strcat(processed_line, " "); // Add a space between tokens
token = strtok(NULL, " \t\n");
printf("%s\n", processed_line); // Output the processed line
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <input_file>\n", argv[0]);
return 1;
FILE *file = fopen(argv[1], "r");
if (!file) {
perror("Error opening file");
return 1;
char line[MAX_LINE_LENGTH];
// Read the file line by line
while (fgets(line, sizeof(line), file)) {
// Check if the line starts with #define (macro definition)
if (strncmp(line, "#define", 7) == 0) {
char name[MAX_MACRO_NAME], replacement[MAX_REPLACEMENT];
// Parse the macro definition
if (sscanf(line, "#define %s %s", name, replacement) == 2) {
define_macro(name, replacement);
} else {
printf("Error: Invalid macro definition: %s", line);
} else {
// Process the line for macro expansion
process_line(line);
fclose(file);
return 0;
Explanation:
1. Macro Structure:
o The Macro structure holds the macro name and the corresponding replacement text.
o We store macros in an array macros[].
2. Define Macro:
o The define_macro() function is used to store a new macro definition.
o It takes the macro name and replacement text as arguments and stores them in the
macros[] array.
3. Find Macro Replacement:
o The find_macro_replacement() function checks if a token in the input text matches a
macro name. If it does, it returns the replacement text; otherwise, it returns NULL.
4. Process Line:
o The process_line() function breaks each line into tokens (words), checks if each token
matches a macro name, and replaces it with the macro’s replacement text if found.
5. File Processing:
o The program reads the file line by line using fgets().
o If the line starts with #define, it defines a new macro by parsing the macro name and
its replacement.
o Otherwise, it processes the line for macro expansion using the process_line()
function.
Input File Example:
Copy code
#define MAX 100
#define PI 3.14
#define HELLO Hello, World!
int main() {
int num = MAX;
float circle = PI;
printf("%s\n", HELLO);
Output (after macro expansion):
arduino
Copy code
int main() {
int num = 100;
float circle = 3.14;
printf("%s\n", Hello, World!);
Key Functions:
fgets(): Reads each line of the file.
sscanf(): Parses the #define macro definitions.
strtok(): Tokenizes each line to process words for macro expansion.
Further Enhancements:
Parameterized Macros: Add support for macros that take arguments.
Recursive Expansion: Handle macros that reference other macros.
Error Handling: Improve handling for malformed macro definitions or recursive macros.
This simple macro processor reads a text file, defines macros, and expands them in the input text.