Thursday, August 30, 2007

The seven sins of programmers

The seven sins of programmers

Fixing bugs in the coder, not the code

By Steven Goodwin

Programmers. The system administrators worship their bit twiddling capabilities. The users exchange vast quantities of beer for new features and tools. And the project managers sell their souls when they make the magic work. But inside the average programmer’s psyche are several demons that need exorcising.

Pride

This is all too common in programmers. Instead of asking whether a particular function exists, or for the best way to retrieve data from the system, a proud programmer is likely to write their own. Especially when faced with a large, or unfamiliar, code base. By re-inventing the wheel there are now two very similar routines. Not only does this increase the code size, it doubles the amount of maintenance required, creates another opportunity for bugs, and adds inconsistency. Later, when another programmer sees these two functions, they will have to choose between them. Which one will depend on their mood (are they also too proud to seek help?), who’s on holiday or who’s outside smoking, at the time! This can equally be applied to duplicate constants, member variables or structures.

Code reviews... must focus on the code, not the coder

Code reviews with a senior team member can help quell a developer’s pride, and guide the other developers in the appropriate direction. This is basic employee training, and one that is simple to implement. Reviews also force the developer to reason each decision made, and justify the creation of new utility routines, perhaps explaining to the lead why existing standard code was not used. To be constructive the review must focus on the code, not the coder, and support free flowing ideas, regardless of the relative seniority of the reviewers. Remember that if the developer is too proud they’ll be closed to ideas, or won’t suggest improvements.

Envy

Programmers should improve themselves by learning from others, not blindly emulating them. All coding methodologies (be they syntactical or design-based) come with caveats. A programmer might inline a function with the pre-processor because he’s seen it done elsewhere, unaware of potential side effects, as in this classic example.

#define SQUARE(x)      x*x

Similar evils occur when programmers move between languages. An experienced developer will typically have a specific style that he tries to crowbar into every other language. Perl written like C. Java written like Ruby. We’ve all seen the examples. Naturally, you should use the best tool for the job, and work to the strengths of that language. By trying to fit idioms from one language into another highlights the fact you understand neither. It’s a development fact of life that some languages are better suited to some tasks, so adapt to it, and close those envious eyes that look at the language you’d rather use. Remember the oft-quoted saying, “When all you have is a hammer, everything looks like a nail”.

Gluttony

Not an evening at the all-you-can-eat buffet, but the problem of writing too much code. Every hour spent coding will require an extra hour of testing, and possibly two more of maintenance. And guess who gets lumbered with the maintenance?

Worse still, this does not scale. A two-minute feature (or one line bug fix) may also take an hour to test. Whatever your gluttonous reasons are—attempts to impress, an under-staffed team, late night camaraderie, or personal pride—curb them. Spending the whole morning trying to unravel last nights two-minute feature is like the buffet—you get stuffed afterwards!

Lust

Programmers crave pleasure; they love to “scratch their own itches”. If unchecked, some developers will write fantastic, innovative, original... and completely unnecessary, code. It could be a new graphics shader, a faster search algorithm or an entire processing engine! If you’re working on anything other than a pet project, you will probably have a schedule that must be adhered to, and a set of pre-determined priorities. These affect the whole project, and not just the wanton lust of an individual developer. Unless you are called Spock, the needs of the many, outweigh the needs of the few... or the one.

Make sure any code written is actually needed. Will it get used? Or will it distract the users who, in turn, add a new plug-in system, just to make use of the code? One very common example of this is premature optimization. Without profiling the code it is impossible to tell exactly where the bottlenecks will occur. Granted, most developers can look at a function and give several reasons why it is slower than it could be, but very few can tell you want percentage of the total running time that function will occupy. Unless it’s known to be on the critical path, then it’s probably not worth optimizing at this time.

The desire to write new code can also exclude the possibility of introducing middleware as a viable solution. The cliché chant is then of “Not Invented Here”. Programmer X once said,

“I’m not using middleware since I don’t understand it; it’ll be just as quick to write it myself. How can I be expected to maintain something I don’t understand?”

He was erroneous for many reasons. Firstly, the initial period of evaluation would give him experience with the code base, and should be a pre-requisite when introducing any new code to a project. Secondly, he’s only considered the development time in terms of coding, when in actuality much of it is taken up with testing—something any sensible middleware product would have in abundance—and as we’ve already seen, writing code doesn’t scale to the subsequent testing and maintenance phases. And finally, there are other trade-offs between write and buy, such as access to other developer forums, paid consultancy, and third party contracts which determine whether the purchase of middleware is a good idea for your specific project.

Curbing programmer lust in this way allows more time spent on the important tasks. This would include writing original, novel, parts of the software and learning the middleware solution itself. After all, it is usually quicker to read, than to write.

Anger

Do not code out of anger. Do not ignore good ideas, regardless from where they come. If a more junior programmer has a solution to the problem in hand—discuss it. If it works—use it! The engine programmer should not be allowed to implement his own solution just because “He’s the engine programmer”.

Anger leads to hate. Hate leads to suffering. To bad code does that lead

Do not code out of spite. Lead programmers: love your coders. Code reviews, for example, should raise the ability of the whole team, and not be used for leads to show off, introduce new jargon, demonstrate obfuscated syntax or exhibit other prima donna characteristics.

Do not code out of fury. Programmers: love your leads. They distribute work because it needs doing. Don’t work on somebody else’s tasks because you’re more suited, or believe it should have been yours. If you want to move into other areas of programming, talk to your lead. Develop prototypes at home. Employing enthusiasm in this manner will win more brownie points than ignoring the task list and schedule.

Sloth

Don’t procrastinate! If a particular piece of code is uninteresting or difficult (like an obscure crash bug), more interesting tasks should be available to compensate. Look forward to those tasks, but don’t daydream about them. If you stop every five minutes for a coffee and chat (or more likely, a whinge) then the task will take much longer, and it will become a self-fulfilling prophecy. Instead, begin with a cup of coffee, a bag of sweets, and your favorite MP3s. Then lose yourself and knuckle down to the task in hand. It won’t be as bad as you think as even dull work makes time pass quickly if you become engrossed in it.

Also, make sure all the tasks are clear, consistent and given from one manager. Opposing requests from different managers will make one of them unhappy, and starting such a doomed task is no fun for anybody.

Greed

There are a couple of places where developers suffer greed. We have already touched on one, and this is a programmer’s innate desire to do too much. The proverbial “biting off more than you can chew” scenario leads to an exponential increase in testing, and a swell of code paths to verify. It can also lead to a lower quality of code since the problem domain may not be well understood by the developer assigned, and the increased workload limits their opportunity to learn.

The final greedy point is directed more towards management, as everyone should feel valued—financially and metaphorically. This is especially true during crunch-time, when even the most junior programmer can work out that their hourly pay packet could be improved by working on the cold meat counter at Tesco! Paying for late night food goes without saying (I hope!), but an occasional pub evening also helps. This gets everyone out the office, and shows that management aren’t greedy with an employee’s time, either. Many ambitious people, regardless of salary, always want more. So even the lead programmer will start questioning their role and think “I’m worth more than this” when they feel unappreciated. How many times do you hear “I’m so overpaid”, compared to “I’m so underpaid”?

Leads should check for warning signs, like “funny” comments in the code—“I’ll do this properly when I get a fscking pay rise!!!”. Management should be wary of stopping or limiting low-cost company perks (such as free soda) since the loss in productivity and willingness to do overtime is undoubtedly greater than the few dollars spent. Follow Dilbert. Less money for staff does not mean more for management.

Naturally, the lead programmer should help prevent such sinful practices. But as responsible professionals, we should all try and curb the devil inside first. Of course, not everyone is Beelzebub incarnate, so score yourself honestly out of ten for each category, and ask a colleague to do the same for you. Then compare. I score 4, 2, 6, 1, 1, 2 and 3 on the categories above, but pride prevents me from letting you in on which scores are for which categories...

A Closer Look at the STL

Vectors Unleashed -- A Closer Look at the STL

Vectors are popular among developers for their efficiency and simplicity. Let us take a look at the ways in which vectors can be created, and the operations that help in inserting, accessing and removing elements from them.

In the last article in this series on advanced C++ programming, we got accustomed to the various components of the standard template library (STL). Now, let us take a closer look at these components. We shall first discuss containers, which are used to store objects. The STL provides various containers that can prove helpful in different situations.

Containers in STL

The containers offered by STL are generic in nature -- hence they are type independent. They include vectors, deques, lists, sets (and multi-sets) and maps (and multi-maps). As mentioned before, vectors are dynamic arrays, and can easily replace ordinary arrays. A deque is similar to a vector, but provides faster insertions and deletions at both ends. Elements are sorted while storing in the case of both sets and multi-sets, which are identical except for the fact that multi-sets can store duplicates, while sets cannot. Maps offer storing elements in terms of key-value pairs. A multi-map differs from a map in providing the flexibility required to store duplicates. In the case of maps, as well, elements are sorted according to some criterion.

Introducing vectors

As mentioned earlier, the std::vector behaves like a dynamic array. By virtue of the generic nature of STLs, we can store any type into a vector. It should be noted that when objects are stored in a vector, they get copied. Hence, these objects should have a default constructor and assignment operator. A destructor should also be provided to facilitate the operation of deletion.

Vectors provide random access to their elements. Also, the iterators used to access the elements of the vector are random access iterators. Hence, these are very compatible with any algorithm provided by the STL library.

Vector creation and initialisation

Having introduced vectors and their characteristics, we shall now look at how we can create and initialise a vector. The STL provides various ways to do this.


1. Creating an empty vector: The code listed below creates a vector having no elements. The default capacity and size are chosen in this case, and are implementation dependent.

#include "vector"

std::vector vecInt;

2. Creating a vector from an existing one: It is possible to create a vector from an existing one. In such a case, all the content of the original vector gets copied to the newly created one. The code snippet listed below illustrates this.

std::vector  vecInt(vecInt1); // vecInt1 is an already

2existing vector

3. Creating a vector with a predetermined size: We can create a vector even if we know only the number of elements it is going to store. This can enhance performance by limiting its dynamic growing capabilities. The code snippet listed below illustrates the creation of a vector, whose size is predetermined.

std::vector  vecInt(1000);

The code above initialises vecInt with 1000 elements. There won’t be any dynamic memory allocation till the vector reaches that size. Some implementations reserve additional memory too -- to enhance performance. Memory allocation is a very important criterion for choosing a particular type of container.

4. Creating a vector with a predetermined size and a value: Taking the method mentioned above a step further, we can also specify a default value to the vector at the time of initialisation. This is illustrated in the code snippet shown below.

std::vector vecInt(1000,27);

Here we initialise a vector with 1000 elements, and each of the elements is initialised with the value, 27.

5. Creating a vector by specifying a range: Initialising a vector by means of another has been mentioned above (refer to case 2). It is also possible to create a vector by specifying a range. A range can be specified by using iterators, as is shown in the code snippet below.

std::vector::iterator itStart = vecInt.begin();

std::vector::iterator itEnd = vecInt.end();

std::vector vecInt2(itStart,itEnd);

The code above yields the same result as in Case 2. The start and the end iterator might point to any location, thus adding flexibility to create a vector from an existing one.
Having discussed the creation of a vector, we shall now look at the various operations supported by vectors. These can be broadly classified into non-modifiable operations and modifiable operations. Non-modifiable operations mainly provide information. Some of the non-modifiable operations are size(), empty(), max_size(), capacity(), etc. Comparison operators are also non-modifiable operations. Modifiable operators change the contents of a particular vector. An assignment operator is one such example. Other modifiable operations are assign() and swap().

Non-modifiable operations

To obtain the size of a vector at any point of time, we can use the size() function.

std::vector  vecInt;
vecInt.push_back(10);

vecInt.push_back(11);

vecInt.push_back(12);

vecInt.push_back(13);
vecInt.size(); // yields 4.

max_size() provides the maximum number of elements the vector can hold, "capacity()" provides the number of elements it can store without reallocation, and "empty()" returns whether the vector is empty or not. Vectors also support the "==", "!=", "<", ">", "<=" and ">=" operators. These have the same meaning as in C++.

Modifiable operators

Assignment operations are modifying operations. The following code snippet shows how the assignment operator can be used.

std::vector  vecInt;

std::vector vecInt1;
vecInt.push_back(10);

vecInt.push_back(11);

vecInt.push_back(12);

vecInt.push_back(13);
vecInt1 = vecInt; // Here contents of vecInt1 are modified to

contain the elements of vecInt.

The STL provides more flexible assignment functionality by providing the assign function for use in the manner shown below.

std::vector  vecInt;

vecInt.assign(10, 27); // This assigns the 10 copies of 27

The assign function can also take a range. We need to provide the start iterator and the end iterator in case a range needs to be specified for this function.

std::vector  vecInt;

std::vector vecInt1;
vecInt.push_back(10);

vecInt.push_back(11);

vecInt.push_back(12);

vecInt.push_back(13);
std::vector::iterator itBegin = vecInt.begin();

std::vector::iterator itEnd = vecInt.end();
vecInt1.assign(itBegin,itEnd); // All the elements from itBegin

to itEnd are assigned to vecInt1

The STL provides the swap faction a functionality that enables the swapping of the elements of two vectors. It comes in two varieties -- first, as a member function of the vector class, and, alternatively, as a global function. The example below illustrates this functionality.

std::vector  vecInt;

std::vector vecInt1;
vecInt.push_back(10);

vecInt.push_back(11);

vecInt.push_back(12);

vecInt.push_back(13);
vecInt1.swap(vecInt); // This swaps all the elements from

vecInt to vecInt1

Accessing the elements of a vector

There are various ways by which elements of a vector can be accessed. The STL provides the subscript operator [], at(), front() and back() to access elements.

std::vector  vecInt;
vecInt.push_back(10);

vecInt.push_back(11);

vecInt.push_back(12);

vecInt.push_back(13);

….

In the above example, vecInt[2] accesses the third element of the vector. There is a disadvantage of using the subscript operator. No range checking is done by default, which could lead to access violations. The STL provides a safer way to access the elements, which is similar to the subscript operator. This functionality is provided by means of the at() operator. The usage is vecInt.at(2). In case the range exceeds the permissible limits, an exception is thrown up. The first element can be accessed by using vecInt.front(), and the last element by using vecInt.back(). The following example illustrates the usage of these functions.

std::vector  vecInt;
vecInt.push_back(10);

vecInt.push_back(11);

vecInt.push_back(12);

vecInt.push_back(13);

vecInt.push_back(14);

vecInt.push_back(15);


std::cout << "Using [] operator :" << vecInt[1] << std::endl;

// yields 11

std::cout << "Using at() : " << vecInt.at(1) << std::endl; //

yields 11

std::cout << "Using front () : " << vecInt.front() <<

std::endl; // yields 10

std::cout << "Using back () :" << vecInt.back() << std::endl;

// yields 15

Inserting and removing elements

Some inserting functions have already been used in the examples above. We shall now look at each one of these in more detail. The STL provides the insert() and push_back() functions to insert elements into the vector. The insert() function has three variants. The code listing below illustrates each of these.

std::vector  vecInt;
vecInt.push_back(10);

vecInt.push_back(11);

vecInt.push_back(12);

vecInt.push_back(13);

vecInt.push_back(14);

vecInt.push_back(15);
std::vector::iterator itBegin = vecInt.begin();

std::vector::iterator itEnd = vecInt.end();
// Case 1

vecInt.insert(itBegin,20); // Will insert the position of

// the element iterator

// position. Here it is at the beginning.
// Case 2

vecInt.insert(itBegin,4,20); // Will insert four copies of 20 at the beginning.
// Case 3

vecInt.insert(itBegin,itBegin,itEnd); // Will insert the whole

of vecInt at the beginning, thereby doubling the size and

duplicating the contents. This is particularly useful when we

have to copy a range from another vector.

The push_back() function inserts elements at the end, and is the most popular way used to insert elements into a vector. The above example illustrates its usage.

The typical remove operations supported by vectors are pop_back(), erase() and clear(). Another modifiable operation of interest is the resize() function, through which we can manually alter the size of the vector. To remove the last element of a vector, we can use the pop_back() function. It does not return the last value -- it just removes the last element. The erase function erases the contents at a particular location pointed to by the iterator. The usage is vecInt.earse(it), where "it" is the position pointed to by the iterator. The erase function can also specify a range to erase. To erase a range, we need to mention the iterator at the start and at the end. The resize function changes the size of the vector to a specified value. Elements are created in the resized vector by means of a default constructor. It is also possible to mention the element to be stored in the resized vector by passing it as the second parameter. To remove the entire contents of a vector, we may use the clear() function. The following example illustrates the usage:

// vecInt is a vector of integers

...

vecInt.pop_back(); // removes the last element

vecInt.erase(it); // erases the element at position ‘it’

vecInt.erase(itStart,itEnd); // erases elements in the range

itStart and itEnd

vecInt.resize(20); // Resizes the size to 20 and fills all the

created elements with the default constructor

vecInt.resize(20,88); // Resizes the size to 20 and fills the

added elements with the value 88

vecInt.clear() // Clears the content of the vector

Before concluding this article on vectors, it seems worthwhile to look at the different iterator functions provided to navigate the contents of a vector. To get the initial position of a vector, begin() can be used. Similarly, to get the end position of a vector, the end() function can be used. The functionality for reverse iteration is also provided by the STL. The counterparts for begin() and end() for reverse iteration are rbegin() and rend().

Vectors provide numerous functions for the efficient storage and management of data. I am sure that the concepts presented in this article will kindle your curiosity to explore vectors even further.

By: Indrajith K. The author is an avid Linux enthusiast having more than six years of experience in software development.

Debugging with GDB

Debugging with GDB: Child's Play!

Check out the basics of the GNU Debugger in the first of two articles on GDB, an open source debugging tool.

For simple programs, debugging without a debugger might be possible by walking through the source code. To do that, you just need to give the debug print statements. But it may not be possible to effectively and efficiently crack problems by doing this for complex programs and systems. This is addressed by the use of debuggers. The debugger is one of the most important tools of any development system.

GDB (GNU Debugger) is one of the most popular open source debugging tools available. The GNU compiler, Emacs Editor and other utilities, work very closely with the GNU debugger. GDB has a command line interface. It supports object file formats like ELF, S-record and many others, and languages like C, C++, etc. In the first of this two-article series, we will focus on the basics of GDB, and in the second part we will cover the advanced features of GDB.

Installation
GDB is available with various Linux distribution CDs. You can obtain the latest binary GDB images from [http://ftp.gnu.org/gnu/gdb/], or if you want the latest GDB RPM, you can find it at [http://rpmfind.net] and install it if needed.

Getting started with GDB

GDB provides a command line interface with a lot of commands to cater to the various needs of debugging, which we will be covering in detail in the following sections. For instance, to debug a C executable you can enter (where mybin is the executable):

# gdb mybin      

Alternatively, you could invoke gdb and then use the file command.

# gdb

(gdb) file mybin
Note that you need to use the gcc -g option to compile your C program so that the debugging information is stored in your executable, which aids in debugging. For instance, let's compile mybin.c with and without debugging information, and see the difference.
% cat mybin.c

#include "stdio.h"

int main(int argc, char** argv) {

printf("test");

return 0;

}
Compiling C programs in debugging mode 
 
% gcc -g mybin.c -o mybin    // here –g opetion will set the program to debug mode
  
$ gdb

(gdb) file mybin

Reading symbols from mybin...done.

(gdb) break 1

Breakpoint 1 at 0x8048460: file mybin.c, line 1.

(gdb) run

Starting program: /home/sekarbc/mybin
Breakpoint 1, main (argc=134513760, argv=0x1) at mybin.c:2

warning: Source file is more recent than executable.
2       int main(int argc, char** argv) {


(gdb) next

main (argc=1, argv=0xbffff8d4) at mybin.c:3

3 printf("test");

(gdb) next

4 return 0;

(gdb) next

5 }

(gdb) next

test

Program exited normally.

Clearly, from the above example, you could see how the gcc --g option helps in creating break points, in walking through the code step by step, and in making use of the debugging symbols that the gcc --g option adds to the executable.

$ gcc mybin.c -o mybin

$ gdb

(gdb) file mybin

Reading symbols from mybin...done.

(gdb) break 1

No line 1 in file "init.c".

(gdb) run

Starting program: /home/sekarbc/mybin

test

Program exited normally.

From the above example you can see that without the gcc --g option, debugging is worse.

GDB commands

When gdb starts, your program is not actually running. It won't run until you tell gdb how to run it. Whenever the prompt appears, you have all the commands on the quick reference sheet available to you.

· run command-line-arguments

Starts your program as if you had typed

a.out command-line arguments

or you can do the following

a.out <>  

to pipe a file as standard input to your program

· break place

Creates a breakpoint; the program will halt when it gets there. The most common breakpoints are at the beginnings of functions, as in

(gdb) break Traverse



Breakpoint 2 at 0x2290: file main.c, line 20

The command break main stops at the beginning of execution. You can also set breakpoints at a particular line in a source file:

(gdb) break 20



Breakpoint 2 at 0x2290: file main.c, line 20

When you run your program and it hits a breakpoint, you'll get a message and prompt like this.

Breakpoint 1, Traverse(head=0x6110, NumNodes=4)



  at main.c:16



(gdb)

In Emacs, you may also use C-c C-b to set a breakpoint at the current point in the program ( the line you have stepped to, for example) or you can move to the line at which you wish to set a breakpoint, and type C-x SPC (Control-X followed by a space).

· delete N

Removes breakpoint number N. Leave off N to remove all breakpoints. info break gives info about each breakpoint

· help command

Provides a brief description of a GDB command or topic. Plain help lists the possible topics

· step

Executes the current line of the program and stops on the next statement to be executed

· next

Like step, however, if the current line of the program contains a function call, it executes the function and stops at the next line.

· step would put you at the beginning of the function

· finish

Keeps doing nexts, without stepping, until reaching the end of the current function

· Continue

Continues regular execution of the program until a breakpoint is hit or the program stops

· file filename

Reloads the debugging info. You need to do this if you are debugging under emacs, and you recompile in a different executable. You MUST tell gdb to load the new file, or else you will keep trying to debug the old program, and this will drive you crazy

· where

Produces a backtrace - the chain of function calls that brought the program to its current place. The command backtrace is equivalent

· print E

prints the value of E in the current framein the program, where E is a C expression (usually just a variable). display is similar, except every time you execute a next or step, it will print out the expression based on the new variable values

· quit

Leave GDB. If you are running gdb under emacs,

C-x 0 

will get you just your code back

The goal of gdb is to give you enough info to pinpoint where your program crashes, and find the bad pointer that is the cause of the problem. Although the actual error probably occurred much earlier in the program, figuring out which variable is causing trouble is a big step in the right direction. Before you seek help from a TA or preceptor, you should try to figure out where your error is occurring

Passing arguments

GDB allows you to pass arguments to the program that is being debugged. You can specify the arguments in the run command when you are in gdb prompt.

For instance, take argtest.c

$ gcc -g argtest.c -o argtest

$ gdb argtest

(gdb) run

Starting program: /home/sekarbc/argtest
You have to use two command line arguments
Program exited with code 0377.

(gdb) run abc def

Starting program: /home/sekarbc/argtest abc def

The first argument is : abc

The second argument is : def
Program exited with code 035.

(gdb)

An alternate method is to use set args command when you are in gdb prompt.

(gdb) file argtest

(gdb) set args abc def

(gdb) run

Starting program: /home/sekarbc/a.out abc def

The first argument is : abc

The second argument is : def
Program exited with code 035.

Working with break points

A break point is a place in the source code file where we temporarily want to stop execution of the program being debugged. Break points can be placed by using the break command. But remember that the program needs to be compiled with the -g option for break points to work. You could use the list keyword to first find out the line number at which to issue the break.

$ gdb sum

(gdb) list

1
#include "stdio.h"

2
int main ()

3
{

4
int num1, num2, total ;

5
printf("Enter first number : ");

6
scanf("%d", &num1);

7
printf("Enter second number : ");

8
scanf("%d", &num2);

9
total = num1 + num2;

10
printf("\nThe sum is : %d\n", total);

To issue a break point at Line 5, we issue:

# (gdb) break sum.c:5

Breakpoint 1 at 0x8048496: file sum.c, line 5.

An alternate way is to set the break point using the function name.

# (gdb) break main

Note: breakpoint 1 also set at pc 0x8048496.

Breakpoint 2 at 0x8048496: file sum.c, line 5.

When you type run in the gdb prompt, instead of the program executing fully, the execution stops in the break point.

# (gdb) run

Starting program: /home/sekarbc/sum
Breakpoint 1, main () at sum.c:5

5
printf("Enter first number : ");

You can find out the information about the break points by typing:

# (gdb) info break

Num Type
Disp Enb Address What

1
breakpoint keep y 0x08048496 in main at sum.c:5

breakpoint already hit 1 time

2
breakpoint keep y 0x08048496 in main at sum.c:5

breakpoint already hit 1 time

Another step before 'continue-ing' Once you have set a break point, it allows you to walk through to the next instruction. Suppose the next instruction is a function sum, step takes execution control through inside the sum function. Continue allows you to continue with the execution of the program. If there is another break point, then it takes you to the break point.

# (gdb) run sum

Starting program: /home/sekarbc/sum sum

Breakpoint 1, main () at sum.c:3

3 {
(gdb) continue

Continuing.
Breakpoint 2, main () at sum.c:6

6 scanf("%d", &num1);
(gdb) run

Starting program: /home/sekarbc/sum sum
Breakpoint 1, main () at sum.c:3

3
{

(gdb) next

main () at sum.c:5

5
printf("Enter first number : ");

(gdb) next
Breakpoint 2, main () at sum.c:6

6 scanf("%d", &num1);
(gdb) run sum  
 (gdb) step

sum (num1=1, num2=2) at sum.c:5

5
return num1+num2;

Examining variables

GDB allows us to display program and environment variables during program execution. We can do this using the print command every time or we can get the values displayed automatically using display command.

(gdb) file sum

A program is being debugged already.
Kill it? (y or n) y
Load new symbol table from "sum"? (y or n) y

Reading symbols from sum...done.

(gdb) break 3

Breakpoint 2 at 0x8048490: file sum.c, line 3.

(gdb) run

Starting program: /home/sekarbc/sum sum
Breakpoint 1, main () at sum.c:11

11
printf("Enter first number : ");

(gdb) n

12
scanf("%d", &num1);

(gdb) print num1

$1 = 134518456

(gdb) n

Enter first number : 5

13
printf("Enter second number : ");

(gdb) print num1

$2 = 5
(gdb) n
The sum is: 20

17
}

3: total = 20

2: num2 = 15

1: num1 = 5

(gdb) run

Starting program: /home/sekarbc/sum
Breakpoint 1, main () at sum.c:11

11
printf("Enter first number : ");

(gdb) display num1

1: num1 = 134518456

(gdb) display num2

2: num2 = 134518236

(gdb) display total

3: total = 134513777

To cap it all

We hope that covering the basics of GDB will help you to sail through the entire process smoothly. This article should help you understand the advanced debugging techniques using GDB, which would be the focus in the next and concluding part of the series.

By: B.C. Sekar and Prakash Varandhani; HCL Technologies -- networking products division.

Saturday, August 4, 2007

Compiling the kernel

Compiling the Linux Kernel


1. Downloading the kernel source code

In order to compile a new kernel we have to download the source code of the Linux kernel. We can download the source from www.kernel.org. Here we can find all versions of the Linux kernel source code. Let's take an example. Suppose we want to compile the 2.6.9 version of the linux kernel. We have to download the 2.6.9 source code from:

http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.9.tar.bz2

It's better to download the bzipped version, as that will be more compressed than its gzipped counterpart; hence will take less time to download. A wget from the command line will look like:

wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.9.tar.bz2

Once we download the required kernel version source, we need to bunzip and untar it. We can do the following:

tar xvjf linux-2.6.9.tar.bz2

The 'x' option is to denote the untarring (e'x'traction), 'v' for verbose, 'j' for specifying that we need to bunzip the file before untarring and 'f' for stating the name of the input file.

The file will untar into the directory linux-2.6.9. Once it's untarred 'cd' to linux-2.6.9.

2. Configuring the kernel

We have to configure the kernel before we start compiling it. During the configuration phase, we will select the components which we want to be part of the kernel. For example: suppose we are using the ext3 filesystem. Then we need to select the ext3 filesystem support while configuring the kernel. Typically we have to run a
make menuconfig
This will bring up the ncurses interface for configuring the kernel. There are other options such as 'make xconfig' and 'make config'. The former will bring up the configuration menu in graphical mode and the latter in text mode.

Once we select the different components we want for our kernel, we can exit the configuration interface. We should select the option to save the configuration from the configuration menu, before exiting.

After we have configured the kernel as mentioned above, we can find a file named '.config' in the top level directory of the source. This file is the configuration file. It contains various options and their states (whether they are selected or not). For example, if we choose to have the PCI support in our kernel we can find an entry of the form:

CONFIG_PCI=y
in the .config file. Similarly, options which are selected as not required will appear as not set. Suppose we have not selected the XFS filesystem support in our kernel we will find the following in the .config
# CONFIG_XFS_FS is not set

A great feature of 2.6 kernels is that if we are running make menuconfig (or xconfig or config) for the first time, then the configuration menu we are presented with is based on our current kernel configuration. In my case, I have a Fedora Core 1 system. The kernel which I run is '2.4.22-1.2115.nptl'. Hence when I run a 'make menuconfig' for the first time on the source then the configuration menu presented will contain the options as given in '/boot/config-2.4.22-1.2115.nptl'.

3. Building Dependencies

This step is required in kernels prior to 2.6 series (here I am only referring to the stable series kernels). For example if we are using a 2.4 kernel then we have to build the dependencies explicitly. We have to run the following:
make dep
This will build the dependencies. But for a 2.6 kernel we can skip this step. The dependencies are automatically created when making the final image with a 2.6 kernel.

4. Creating the final image

We can build various types of kernel binary images. We can build a plain kernel image, or a compressed version of it; the usual choice is compressed, or the 'bzImage'. We can create the bzImage by running
make bzImage
In 2.6 kernels this step will also resolve the dependencies and proceed to create a bzImage image.

After the compilation is over we can find the kernel image at the path arch/i386/boot/bzImage in case of an image for a 386 based processor (Pentium, AMD etc.).

5. Compiling and Installing the modules

In the configuring section if we have selected some components to be built as kernel modules then we need to compile those modules. To compile the modules we should run the command:
make modules
This command will compile the components (which are selected for module compilation) to modules. In a 2.4 kernel the result will be .o files of the corresponding components. But in a 2.6 kernel the output file will be a .ko module. For example if we have given the option for the Network driver of Realtek cards to be built as modules then after giving a 'make modules' we can find in 'driver/net/' a file named 8139too.o in the case of a 2.4 kernel and 8139too.ko in the case of a 2.6 kernel.

After we have compiled the modules, it's time now to install the modules. To install the modules run:

make modules_install
as root. This will install the modules and other necessary files into the /lib/modules/2.6.9 directory.

6. Booting from the new kernel

Once we are done with the installation of modules, we can go for an automatic installation procedure for the kernel binary. We just have to run
make install
This will update the kernel image on to the /boot area, update the configuration file of the bootloader (lilo.conf or grub.conf) and then do the necessary actions to make the new kernel bootable.

After this we need to reboot the machine. When the machine boots next time the boot menu will present us with the option to boot from the new kernel we built. We choose that option and voila!! boot into a kernel we built all by ourselves!

7. Manual installation of the kernel

In case 'make install' does not work, or if we cannot perform an automatic installation due to some other reason, we can go for a manual installation of the kernel. For example, if we are using the grub boot loader then we have to copy the bzImage into the boot partition and then change the '/etc/grub.conf' to reflect the presence of the new image. If we are having lilo boot loader then we have to copy the bzImage to the boot location and then modify the lilo.conf and then run the 'lilo' command to make sure that next time we boot we will have our new image as a choice to boot from. The following are the steps we should perform as root user if we are using lilo boot loader:
 cp -a arch/i386/boot/bzImage /boot/bzImage-2.6.9 
After this we add the following entry to /etc/lilo.conf
image=/boot/bzImage-2.6.9
label=2.6.9-kernel
root=your_root_disk
We should run lilo after this
lilo -v
We will reboot the machine after this. When we are prompted at the lilo prompt enter '2.6.9-kernel' as the boot option and we will be booting to the new custom built kernel.

8. Cleaning the kernel source

After we have initiated compilation once on the source if we want to clean the object files and other temporary files then we have to run the following:
make clean
This will remove most generated files but will keep the configuration file.

If we need an absolute cleaning, i.e. if we want to return the source to the state in which it was before we started the compilation, then do a

make mrproper
This command will delete all generated files, the configuration file as well as various backup files. This will in effect unwind all the changes we made to the source.