Advanced C: - Sushma - Ukil
Advanced C: - Sushma - Ukil
-Sushma
-Ukil
Memory Management
• A process is an "invocation" or "activation" of a program. A program is a
list of instructions for execution by a computer.
• To run the program it needs to be copied (or loaded) into the main
computer memory and the central processing unit is told to start reading
(and obeying) instructions taken from that area of memory. The activity of
executing the program's instructions is called running the process.
• A portion of memory needs to be allocated to a process. The actual
allocation of such an area of memory is a task for the loader.
• The memory area allocated to a program will usually be split into several
sub-areas for particular.
Memory management
The code segment
• This is known as the text area in Unix parlance and simply contains the
executable code of the program(both the program binary and any shared
library it loads).
• If there are several processes running the same program there will still
only be one code area as it is identical for all processes.
• The current state of a process is determined by its various data areas.
• All memory pages of code segment are marked as read-only and are
shared with any other process using the same program file and/or shared
library files.
• The kernel process exchange (or switch) mechanism will activate a
particular process by indicating to the hardware where the next
instruction is to be read from what mapping is to be used to access data
Memory Management
The data segment
• This holds the data being processed by the program, it's size is initially
determined by the loader from information in the binary file which
specifies the amount of initialized data to be copied form the binary file
and the amount of un initialized space to be allocated to the process.
• On some systems the space may be initialized to zero but this is not
universal and such areas may contain "droppings" left over from previous
processes, a possible security problem. On older Unix systems the un
initialized space is known as bss from a PDP/11 assembler mnemonic.
• The Unix size command will give the memory requirement information
for a binary file.
• This segment can grow as the process runs and needs more virtual
memory.
• bash$ size testhand2 92763 + 7564 + 2320 = 102647
• The above example shows that loading the binary file testhand2 will
require 102647 bytes of memory with 92763 bytes for code, 7564 bytes
for initialized data and 2320 bytes for non-initialized data.
Memory Management
The stack segment
Stack
• The stack portion of memory, on the other hand, is used by the process
to aid the invocation of functions.
• Every time a function is called, the process reserves a portion of stack
memory to store the values of parameters passed to the functions as
well as for results returned by the functions and the local variables used
within the functions.
• The stack is also where space for all declared data types and structures
is reserved at compile time.
Memory Management
The Stack And Local Variables
• The stack is used by the process to store the chain of functions which are
currently in the middle of execution.
• Each function call causes the process to add an execution frame to the
top of the stack. This execution frame would contain the contents of
CPU registers as they were before the function call was made, the return
address (i.e. where in the code this function was invoked, so we can
return there when the function returns), the parameters passed to the
function and all local variables of the function.
• Thus, when we have function 'main' calling function 'a' which calls
function 'b', we will have 3 frames on the stack.
• The 'allocation' of a stack frame does not require performing real
memory allocation - as the space for the stack was reserved during
process startup. Instead, this is done by merely marking a part of the
stack as containing the frame, and copying data to that frame.
• Note that local variables are "allocated" by simply advancing the stack
pointer beyond their location on the stack, so allocating local variables
takes a fixed amount of time, no matter their size.
• When a function returns - its stack frame is freed by simply modifying
the stack pointer to point below its frame.
Memory Management
The Stack And Local Variables(cont..)
• The local variables are not initialized - they just contain the values that
were accidentally placed in the memory locations these variables
occupy.
• Consider a situation in which a function was called, its variables used
and given some values.
• Later the function returned, and its frame was released.
• Then, another function was called.
• The stack frame of this new function is located in the same place in
memory as the frame of the former function, so the new function's local
variables will get the values that were left there by the local variables of
the former function.
• This can explain why the values of un initialized local variables are often
neither 0, nor look like total garbage.
• Since all local variables are stored in consecutive memory on the stack,
if we take a pointer to such a variable, and later on write right above or
right below this pointer, we'll be modifying the contents of another local
variable, or even that of the function's return address.
Memory Management
The Stack And Local Variables(cont..)
• For example, consider the following code:
int foo()
{
int numbers[2];
int j; j = 2;
printf("j - %d\n", j);
numbers[2] = 3;
printf("j - %d\n", j);
}
• During execution of this function, we first assign 2 to 'j', and thus the first
print command will show "j - 2". Then, we try to assign a value to
'numbers[2]'. However, the 'numbers' array only has 2 cells - 0 and 1.
Writing into subscript '2' of this array will cause us to write just beyond
the array (which is fully located on the stack).
• The variable 'j' just happens to be stored in that location in memory, and
thus, the value '3' will be actually assigned to 'j'.
• Our second print command will thus show "j - 3".
Memory Management
The Stack And Local Variables(cont..)
• Note that this assumes that the variables are stored in memory in the
same order as they were declared inside the function's code. With
some compilers, this might not be the case, and the out-of-range
assignment might overwrite a different variable, or a part of the stack
that does not hold variables, leading to other unexpected results.
• Note: local variables (as well as function parameters) might be stored
in registers, rather than on the stack. This could be either because we
used the 'register' keyword when declaring these variables, or because
the compiler's optimization chose to place a variable in a register. Of
course, such variables cannot be over-written by stack overflows.
Memory Management
Dynamic Memory Allocation
• Memory can be allocated dynamically by the calls
– malloc()
– calloc()
– realloc()
• The prototype for malloc is:
void *malloc(size_t size);
• malloc takes in a size_t and returns a void pointer.
• Why does it return a void pointer? Because it doesn't matter to malloc to
what type this memory will be used for.
• Let's see an example of how malloc is used:
int *ip;
ip = malloc(5 * sizeof(int)); /* .. OR .. */
ip = malloc(5 * sizeof(ip));
• Pretty simplistic. sizeof(int) returns the sizeof an integer on the machine,
multiply by 5 and malloc that many bytes.
• The second malloc works because it sends what ip is pointing to, which
is an int.
Memory Management
Dynamic Memory Allocation(cont..)
• Wait... we're forgetting something. AH! We didn't check
for return values.
• Here's some modified code:
#define INITIAL_ARRAY_SIZE 5
/* ... code ... */
int *ip;
if ((ip = malloc(INITIAL_ARRAY_SIZE * sizeof(int))) == NULL)
{
(void)fprintf(stderr, "ERROR: Malloc failed");
(void)exit(EXIT_FAILURE); /* or return EXIT_FAILURE; */
}
• Now our program properly prints an error message and
exits gracefully if malloc fails.
Memory Management
Dynamic Memory Allocation(cont..)
• calloc works like malloc, but initializes the memory to zero
if possible.
• The prototype is:
void *calloc(size_t nmemb, size_t size);
strncpy
strncpy is similar to strcpy, but it allows the number of characters to
be copied to be specified. If the source is shorter than the destination,
than the destination is padded with null characters up to the length
specified. This function returns a pointer to the destination string, or a
NULL pointer on error. Its prototype is:
char *strncpy(char *dst, const char *src, size_t len);
strcat
This function appends a source string to the end of a destination
string. This function returns a pointer to the destination string, or a
NULL pointer on error. Its prototype is:
char *strcat(char *dst, const char *src);
Strings
Common String Functions(Cont..)
strncat
This function appends at most N characters from the source string to the end
of the destination string. This function returns a pointer to the destination
string, or a NULL pointer on error. Its prototype is:
char *strncat(char *dst, const char *src, size_t N);
strcmp
This function compares two strings. If the first string is greater than the
second, it returns a number greater than zero. If the second string is greater, it
returns a number less than zero. If the strings are equal, it returns 0. Its
prototype is:
int strcmp(const char *first, const char *second);
strncmp
This function compares the first N characters of each string. If the first string
is greater than the second, it returns a number greater than zero. If the second
string is greater, it returns a number less than zero. If the strings are equal, it
returns 0. Its prototype is:
int strncmp(const char *first, const char *second, size_t N);
Strings
Common String Functions(Cont..)
strlen
This function returns the length of a string, not counting the null character
at the end. That is, it returns the character count of the string, without the
terminator. Its prototype is:
size_t strlen(const char *str);
strchr
This function finds the first occurrence of character C in string STR,
returning a pointer to the located character. It returns NULL pointer
when there is no match
char * strchr (char *STR, char C)
strstr
This function is like `strchr' but searches for string SEARCH in string
STR, returning a pointer into the STR that is the first character of the
match found in STR. It returns NULL pointer if no match was found. If
SEARCH is an empty string, it returns STR.
char * strstr (const char *STR, const char *SEARCH)
Bitwise Operators
• Bitwise operators apply the same operation to matching bits in value on
each side of operator (the one's complement is unary and works only
on the value to it's right)
• Result for each bit position determined only by the bit(s) in that
position
• Results for each operator summarized in the following table
a 0 0 1 1
b 0 1 0 1
and a&b 0 0 0 1
or a|b 0 1 1 1
exclusive or a^b 0 1 1 0
one's complement ~a 1 1 0 0
Bitwise Operators
Shift Operators
• Operators >> and << can be used to shift the bits of an operand to the right
or left a desired number of positions
• The number of positions to be shifted can be specified as a constant, in a
variable or as an expression
• Can be used on any of the integral data types - char, short, <VARINT<
VAR>or long
• Bits shifted out are lost
• For left shifts, bit positions vacated by shifting always filled with zeros
• For right shifts, bit positions vacated by shifting filled
– with zeros for unsigned data type
– with copy of the highest (sign) bit for signed data type
• Applications
– quick multiplication by a power of 2
– quick division by power of 2 (unsigned types only)
– bit-mapped record processing
– packing / unpacking data less that byte size
Bitwise Operators
Shift operators(contd..)
• Can use << to multiply integral value by a power of two
• 1 bit multiplies by 2, 2 bits multiplies by 4, 3 bits multiplies by 8, n bits multiplies by
2n
• On some implementations, shifting may be much faster than multiplying (but good
ol' multiplication makes for much clearer code)
• Using left shift to multiply:
long x = 75, y;
int i;
...
x <<= 2; /* x = 75 * 4 = 300 */
y = x << 1; /* y = ( x * 2 = 600 ) x is not changed */
x = 6;
for ( i = 0; i < 5; i++ )
{
printf( "%d\n", x << i );
}
Prints
6 6 << 0 is 6*1
12 6 << 1 is 6*2
24 6 << 2 is 6*4
48 6 << 3 is 6*8
96 6 << 4 is 6*16
Bitwise Operators
Shift operators(contd..)
• Can use >> to divide unsigned integral value by a power of two.
• 1 bit divides by 2, 2 bits divides by 4, 3 bits divides by 8, n bits
divides by 2n
• On some implementations, shifting may be much faster than dividing
(but division makes for much clearer code)
• Shifting signed values may fail because for negative values the result
never gets past -1:
• -5 >> 3is -1 and not 0 like -5/8
• Using right shift to divide:
unsigned long x = 75, y;
...
x >>= 2; /* x = 75 / 4 = 18 */
y = x >> 1; /* y = ( x / 2 = 9 ) x is not changed */
Structures
• Structure in C is a collection of items of different types. You can think
of a structure as a "record" is in Pascal or a class in Java without
methods.
• So how is a structure declared and initialized? Let's look at an example:
struct student
{
char *first;
char *last;
char SSN[9];
float gpa;
char **classes;
};
struct student student_a, student_b;
Structures :: Declaration and Syntax
Structures
• Another way to declare the same thing is:
struct
{
char *first;
char *last;
char SSN[10];
float gpa;
char **classes;
} student_a, student_b;
• As you can see, the tag immediately after struct is optional. But in the
second case, if you wanted to declare another struct later, you couldn't.
Structures
• The "better" method of initializing structs is:
struct student_t
{
char *first;
char *last;
char SSN[10];
float gpa;
char **classes;
} student, *pstudent;
• Now we have created a student_t student and a
student_t pointer. The pointer allows us greater
flexibility (e.g. Create lists of students).
• You could initialize a struct just like an array
initialization. But be careful, you can't initialize this
struct at declaration time because of the pointers.
• To access fields inside struct C has a special operator for
this called "member of" operator denoted by . (period).
For example, to assign the SSN of student_a:
strcpy(student_a.SSN, "111223333\0");
Structures
Nested structures
• Structures can contain other structures as members; in other words,
structures can nest. Consider the following two structure types:
struct first_structure_type
{
int integer_member;
float float_member;
};
struct second_structure_type
{
double double_member;
struct first_structure_type struct_member;
};
• The first structure type is incorporated as a member of the second
structure type.
Structures
Nested structures(Contd..)
• You can initialize a variable of the second type as follows:
struct second_structure_type demo;
• demo.double_member = 12345.6789;
demo.struct_member.integer_member = 5;
demo.struct_member.float_member = 1023.17;
• The member operator . is used to access members of structures that are
themselves members of a larger structure.
• No parentheses are needed to force a special order of evaluation
• A member operator expression is simply evaluated from left to right.
• In principle, structures can be nested indefinitely.
Typedef
• There is an easier way to define structs or you could "alias" types you
create.
• For example:
typedef struct
{
char *first;
char *last;
char SSN[9];
float gpa;
char **classes;
} student; student student_a;
• Now we get rid of those silly struct tags. You can use typedef for non-
structs: typedef long int *pint32; pint32 x, y, z; x, y and z are all
pointers to long ints. typedef is your friend. Use it.
Union
• Unions are declared in the same fashion as structs, but have a
fundamental difference. Only one item within the union can be used at
any time, because the memory allocated for each item inside the union
is in a shared memory location. Why you ask? An example first:
struct conditions
{
float temp;
union feels_like
{
float wind_chill;
float heat_index;
}
} today;
• As you know, wind_chill is only calculated when it is "cold" and
heat_index when it is "hot". There is no need for both. So when you
specify the temp in today, feels_like only has one value, either a float
for wind_chill or a float for heat_index.
• Types inside of unions are unrestricted, you can even use structs within
unions.
Enum
• What if you wanted a series of constants without creating a new type?
Enter enumerated types. Say you wanted an "array" of months in a
year:
enum e_months {JAN=1, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC};
typedef enum e_months month;
month currentmonth;
currentmonth = JUN; /* same as currentmonth = 6; */
printf("%d\n", currentmonth);
• We are enumerating the months in a year into a type called month.
You aren't creating a type, because enumerated types are simply
integers. Thus the printf statement uses %d, not %s.
• If you notice the first month, JAN=1 tells C to make the enumeration
start at 1 instead of 0.
• Note: This would be almost the same as using:
• #define JAN 1
• #define FEB 2
• #define MAR 3 /* ... etc ... */
Static Variables
• A static variable is local to particular function. However, it is only
initialised once (on the first call to function).
• Also the value of the variable on leaving the function remains intact.
On the next call to the function the the static variable has the same
value as on leaving.
• To define a static variable simply prefix the variable declaration with
the static keyword. For example:
• void stat(); /* prototype fn */ main() { int i; for (i=0;i<5;++i)
stat(); } stat() { int auto_var = 0; static int static_var = 0;
printf( ``auto = %d, static = %d n'', auto_var, static_var); +
+auto_var; ++static_var; }
• Output is:
• auto_var = 0, static_var= 0 auto_var = 0, static_var = 1 auto_var =
0, static_var = 2 auto_var = 0, static_var = 3 auto_var = 0, static_var
=4
• NOTE:
• Only n lower bits will be assigned to an n bit number. So
type cannot take values larger than 15 (4 bits long).
• Bit fields are always converted to integer type for
computation.
• You are allowed to mix ``normal'' types with bit fields.
• The unsigned definition is important - ensures that no bits
are used as a flag.
Bitfields
Bit Fields: Practical Example
• Frequently device controllers (e.g. disk drives) and the operating system need to
communicate at a low level. Device controllers contain several registers which may
be packed together in one integer
• We could define this register easily with bit fields:
struct DISK_REGISTER
{
unsigned ready:1;
unsigned error_occured:1;
unsigned disk_spinning:1;
unsigned write_protect:1;
unsigned head_loaded:1;
unsigned error_code:8;
unsigned track:9; u
nsigned sector:5;
unsigned command:5; };
• To access values stored at a particular memory address,
DISK_REGISTER_MEMORY we can assign a pointer of the above structure to
access the memory via:
• struct DISK_REGISTER *disk_reg = (struct DISK_REGISTER *)
DISK_REGISTER_MEMORY;
Bitfields
• The disk driver code to access this is now relatively straightforward:
/* Define sector and track to start read */
disk_reg->sector = new_sector;
disk_reg->track = new_track;
disk_reg->command = READ; /* wait until operation done, ready will
be true */
while ( ! disk_reg->ready ) ; /* check for errors */
if (disk_reg->error_occured)
{ /* interrogate disk_reg->error_code for error type */
switch (disk_reg->error_code) ......
}
A note of caution: Portability
• Bit fields are a convenient way to express many difficult operations.
However, bit fields do suffer from a lack of portability between
platforms
• integers may be signed or unsigned
Bitfields
• Many compilers limit the maximum number of bits in the bit field
to the size of an integer which may be either 16-bit or 32-bit
varieties.
• Some bit field members are stored left to right others are stored
right to left in memory.
• If bit fields too large, next bit field may be stored consecutively in
memory (overlapping the boundary between memory locations)
or in the next word of memory.
• If portability of code is a premium you can use bit shifting and
masking to achieve the same results but not as easy to express or read.
For example:
• unsigned int *disk_reg = (unsigned int *)
DISK_REGISTER_MEMORY; /* see if disk error occured */
disk_error_occured = (disk_reg & 0x40000000) >> 31;
Typecasting
• C is one of the few languages to allow coercion, that is forcing one
variable of one type to be another type. C allows this using the cast
operator (). So:
int integernumber;
float floatnumber=9.87;
integernumber=(int)floatnumber;
• And:
int integernumber=10;
float floatnumber;
floatnumber=(float)integernumber;
} i
a
Rules
• A rule tells Make both when and how to make a file. As an example, suppose your
project involves compiling source files main.c and io.c then linking them to produce the
executable project.exe. Withholding a detailed explanation for a bit, here is a makefile
using Borland C which will manage the task of making project.exe:
• The Example Makefile
project.exe : main.obj io.obj
tlink c0s main.obj io.obj, project.exe,, cs /Lf:\bc\lib
main.obj : main.c
bcc –ms –c main.c
io.obj : io.c
bcc –ms –c io.c
• This makefile shows three rules, one each for making project.exe, main.obj, and io.obj.
The rules as shown above are called explicit rules since they are supplied explicitly in the
makefile. Make also has inference rules that generalize the make process.
Make and Makefiles
Dependency Lines: When to Build a Target
• The lines with the colon “:” in them are called dependency
lines. They determine when the target is to be rebuilt.
• To the left of the colon is the target of the dependency. To
the right of the colon are the sources needed to make the
target. A dependency line says “the target depends on the
sources.” For example, the line:
project.exe : main.obj io.obj
• states that project.exe depends on main.obj and io.obj. At
run time Make compares the time that project.exe was last
changed to the times main.obj and io.obj were last changed.
If either source is newer than project.exe, Make rebuilds
project.exe. The last-changed time is the target's time as it
appears in the file-system directory. This time is also known
as the target's timestamp.
Make and Makefiles
The Make Process is Recursive
• It is a basic feature of Make that a target's sources are made before the
timestamp comparison occurs. The line:
project.exe : main.obj io.obj
• implies “make main.obj and io.obj before comparing their timestamps
with project.exe.” In turn:
main.obj : main.c
• says “make main.c before comparing its timestamp with main.obj.”
You can see that if main.c is newer than main.obj, main.obj will be
rebuilt. Now main.obj will be newer than project.exe, causing
project.exe to be rebuilt.
Additional Dependencies
• In C and in other programming languages it is possible to include the
contents of a file into the file currently being compiled. Since the
compiled object depends on the contents of the included file, we add
the included file as a source of the object file.
Make and Makefiles
• Assume each of main.c and io.c include def.h. We can either change
two dependency lines in the makefile:
main.obj : main.c becomes main.obj : main.c def.h
io.obj : io.c becomes io.obj : io.c def.h
• or add a new line which lists only the additional dependencies:
main.obj io.obj : def.h
• Notice that there are two targets on the left of the colon. This line
means that both main.obj and io.obj depend on def.h. Either of these
methods are equivalent. The example makefile now looks like:
project.exe : main.obj io.obj
tlink c0s main.obj io.obj, project.exe,, cs /Lf:\bc\lib
main.obj : main.c
bcc –ms –c main.c
io.obj : io.c
bcc –ms –c io.c
main.obj io.obj : incl.h
Make and Makefiles
Shell Lines: How to Build a Target [Top]
• The indented lines that follow each dependency line are called shell lines.
Shell lines tell Make how to build the target. For example:
project.exe : main.obj io.obj
tlink c0s main.obj io.obj, project.exe,, cs /Lf:\bc\lib
• tells Make that making project.exe requires running the program tlink to link
main.obj and io.obj. This shell line would be run only if main.obj or io.obj was
newer than project.exe.
• For tlink, c0s is the small model start-up object file and the cs is the small
model library. The /Lf:\bc\lib flag tells tlink that the start-up object file and
library files can be found in the f:\bc\lib directory.
• A target can have more than one shell line, listed one after the other, such as:
project.exe : main.obj io.obj
echo Linking project.exe
tlink c0s main.obj io.obj, project.exe,, cs /Lf:\bc\lib >tlink.out
• The first line shows that command processor commands can be executed by
Make. The second line shows redirection of output, where the output of the
tlink program is redirected to the tlink.out file.
Make and Makefiles
• After each shell line is executed, Make checks the shell line exit
status. By convention, programs return a 0 (zero) exit status if they
finish without error and non-zero if there was an error. The first shell
line returning a non-zero exit status causes Make to display the
message:
OPUS MAKE: Shell line exit status exit_status. Stop.
• This usually means the program being executed failed. Some
programs return a non-zero exit status inappropriately and you can
have Make ignore the exit status by using a shell-line prefix. Prefixes
are characters that appear before the program name and modify the
way Make handles the shell line. For example:
project.exe : main.obj io.obj
– tlink c0s main.obj io.obj, project.exe,, cs /Lf:\bc\lib
• The “–” prefix tells Make to ignore the exit status of shell line. If the
exit status was non-zero Make would display the message:
OPUS MAKE: Shell line exit status exit_status (ignored)
Make and Makefiles
Macros
• The example makefile is reproduced here:
project.exe : main.obj io.obj
tlink c0s main.obj io.obj, project.exe,, cs /Lf:\bc\lib
main.obj : main.c
bcc –ms –c main.c
io.obj : io.c
bcc –ms –c io.c
main.obj io.obj : def.h
• We see that the text “main.obj io.obj” occurs repeatedly. To cut down
on the amount of repeated text, we can use a macro definition to assign
a symbol to the text.
Defining Macros in the Makefile
• A macro definition line is a makefile line with a macro name, an equals
sign “=”, and a macro value. In the makefile, expressions of the form $
(name) or ${name} are replaced with value. If the macro name is a
single letter, the parentheses or braces are optional (i.e. $X, $(X) and $
{X} all mean “the value of macro X”).
Make and Makefiles
• Here is the above example written with the introduction of four macros:
OBJS = main.obj io.obj
MODEL = s
CC = bcc
CFLAGS = –m$(MODEL)
project.exe : $(OBJS)
tlink c0$(MODEL) $(OBJS), project.exe,, c$(MODEL) /Lf:\bc\lib
main.obj : main.c
$(CC) $(CFLAGS) –c main.c
io.obj : io.c $(CC) $(CFLAGS) –c io.c
$(OBJS) : incl.h
• The value of the OBJS macro is the list of object files to be compiled.
The macro definitions for MODEL, CC and CFLAGS were introduced
so that it is easier to change the compiler memory model, name of the C
compiler and its options.
• Make automatically imports environment variables as macros, so you
can reference an environment variable such as PATH with the makefile
expression $(PATH).
Make and Makefiles
Defining Macros on the Command Line
• Macros can be defined on the Make command line. For example:
make CFLAGS=–ms
• would start up Make and define the macro CFLAGS with the value “–
ms”. Macros defined on the command line take precedence over macros
of the same name defined in the makefile.
• If a command-line macro contains spaces, it must be enclosed in double
quotes as in:
make "CFLAGS=-ms -z -p"
Run-Time Macros
• Make defines some special macros whose values are set dynamically.
These macros return information about the current target being built. As
examples, the .TARGET macro is name of the current target, the
.SOURCE macro is the name of the inferred source (from an inference
rule) or the first of the explicit sources and the .SOURCES macro is the
list of all sources.
Make and Makefiles
• Using run-time macros the example can be written:
OBJS = main.obj io.obj
CC = bcc
MODEL = s
CFLAGS = –m$(MODEL)
project.exe : $(OBJS)
tlink c0$(MODEL) $(OBJS), $(.TARGET),, c$(MODEL)
/Lf:\bc\lib
main.obj : main.c
$(CC) $(CFLAGS) –c $(.SOURCE)
io.obj : io.c
$(CC) $(CFLAGS) –c $(.SOURCE)
$(OBJS) : incl.h
• As you can see, the shell lines for updating main.obj and io.obj are
identical when run-time macros are used. Run-time macros are
important for generalizing the build process with inference rules.
Make and Makefiles
Macro Modifiers
• Macros are used to reduce the amount of repeated text. They are also
used in inference rules to generalize the build process. We often want
to start with the value of a macro and modify it in some manner. For
example, to get the list of source files from the OBJS macro we can do:
SRCS = $(OBJS,.obj=.c)
• This example uses the “from=to” macro modifier to replace the from
text in the expansion of OBJS with the to text. The result is that $
(SRCS) is “main.c io.c”. In general, to modify a macro expand it with:
$(name,modifier[,modifier ...])
• Each modifier is applied in succession to the expanded value of name.
Each modifier is separated from the next with a comma.
Filename Components
• There is a set of macro modifiers for accessing parts of file names. For
example, with the macro definition:
SRCS = d:\src\main.c io.asm
Make and Makefiles
• Some of the modifiers are:
Modifier, and description Example Value
D, the directory $(SRCS,D) d:\src .
E, the extension (or suffix) $(SRCS,E) .c .asm
F, the file name $(SRCS,F) main.c io.asm
Tokenize
• Another modifier is the “Wstr” modifier, which replaces whitespace
between elements of the macro with str, a string. The str can be a mix
of regular characters and special sequences, the most important
sequence being “\n” which represents a newline character (like hitting
the enter key). For example:
$(OBJS,W space +\n) is main.obj +
io.obj
Other Modifiers
• Other modifiers include: “@” (include file contents), “LC” (lower
case), “UC” (upper case), “M” (member) and “N” (non-member). The
“M” and “N” modifiers and the “S” (substitute) modifier use regular
expressions for powerful and flexible pattern-matching. See Page for
more information on all macro modifiers.
Make and Makefiles
Inference Rules
• Inference rules generalize the build process so you don't have to give
an explicit rule for each target. As an example, compiling C source (.c
files) into object files (.obj files) is a common occurrence. Rather than
requiring a statement that each .obj file depends on a like-named .c file,
Make uses an inference rule to infer that dependency. The source
determined by an inference rule is called the inferred source.
• Inference rules are rules distinguished by the use of the character “%”
in the dependency line. The “%” (rule character) is a wild card,
matching zero or more characters. As an example, here is an inference
rule for building .obj files from .c files:
%.obj : %.c
$(CC) $(CFLAGS) –c $(.SOURCE)
• This rule states that a .obj file can be built from a corresponding .c file
with the shell line “$(CC) $(CFLAGS) –c $(.SOURCE)”. The .c and
.obj files share the same root of the file name.
• When the source and target have the same file name except for their
extensions, this rule can be specified in an alternative way:
.c.obj :
$(CC) $(CFLAGS) –c $(.SOURCE)
Make and Makefiles
• The alternative form is compatible with Opus Make prior to
this version and with other make utilities and is discussed in
more detail on Page .
• Make predefines the “%.obj : %.c” inference rule as listed
above so the example we have been working on now
becomes much simpler:
OBJS = main.obj io.obj
CC = bcc
MODEL = s
CFLAGS = –m$(MODEL)
project.exe : $(OBJS)
tlink c0$(MODEL) $(OBJS), $(.TARGET),, c$(MODEL)
/Lf:\bc\lib
$(OBJS) : incl.h
Make and Makefiles
Response Files
• For MS-DOS, OS/2 & Win95 there is a rather severe restriction on the
length of a shell line with the result that the shell line is often too short
for many compilers and far too short for linkers and librarians.
• To overcome this restriction many programs can receive command-line
input from a response file. Opus Make has two kinds of support for
response files: automatic response files, where Make decides when to
build a response file or; inline response files, where you write response
file-creating statements directly in the makefile.
Automatic Response Files
• Make has predefined support for several linkers, librarians and
compilers, and you can augment Make's support by writing your own
definitions. With Make's predefined support you can just add the
following statement to your makefile:
.RESPONSE.LINK : tlink
• This tells Make that the program tlink accepts LINK-style response files.
When a shell line executes tlink, Make checks if the shell line is longer
than allowed by the operating system and automatically produces a
response file if necessary.
Make and Makefiles
Inline Response Files
• Response files can also be coded “inline” in your makefile. Here is the
tlink shell line of the example, written to use an inline response file:
project.exe : $(OBJS)
tlink @<<
c0$(MODEL) $(.SOURCES,W+\n)
$(.TARGET)
c$(MODEL) /Lf:\bc\lib
<<
• The tlink program is invoked as “tlink @response_file” where
response_file is a name generated by Make. The “W+\n” macro
modification replaces whitespace between elements of $(.SOURCES)
with “+enter”. The response_file contains:
c0s main.obj+
io.obj
project.exe
c0 /f:\bc\lib
Make and Makefiles
Makefile Directives
• Makefile directives control the makefile lines Make reads at read time. Here is our
example extended with conditional directives (%if, %elif, %else and %endif) to
support both Borland and Microsoft compilers. Comments have been added for
documentation:
# This makefile compiles the project listed in the PROJ macro
#
PROJ = project # the name of the project
OBJS = main.obj io.obj # list of object files
# Configuration:
#
MODEL = s # memory model
CC = bcc # name of compiler
# Compiler-dependent section
#
%if $(CC) == bcc # if compiler is bcc
CFLAGS = –m$(MODEL) # $(CFLAGS) is –ms
LDSTART = c0$(MODEL) # the start-up object file
LDLIBS = c$(MODEL) # the library
LDFLAGS = /Lf:\bc\lib # f:\bc\lib is library directory
Make and Makefiles
%elif $(CC) == cl # else if compiler is cl
CFLAGS = –A$(MODEL,UC) # $(CFLAGS) is –AS
LDSTART = # no special start-up
LDLIBS = # no special library
LDFLAGS = /Lf:\c6\lib; # f:\c6\lib is library directory
%else # else
% abort Unsupported CC==$(CC) # compiler is not supported
%endif # endif