Jump to content

  • Log In with Google      Sign In   
  • Create Account

Like
0Likes
Dislike

Fun with Linux - Article 1/5

By Jered Wierzbicki | Published Jul 05 2000 12:43 PM in Game Programming

gcc unix compiler program linux dos signals printf #include
If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

This is article one in a series of five. Each article will be comprised of five sample programs with analyses and tips. Article one talks about basic UNIX programming concepts and assumes a solid understanding of ANSI C; a DOS or Win32 background is a plus. The next article in the series will introduce game programming under Linux and issues involved.


Program 1.

The best way to get started programming for any new platform is to...? Exactly. There is no best way--there is only tradition. And, in honor of tradition, I will show you what is hopefully a very familiar program.

#include <stdio.h>

main() {
	printf("Hello, world!\n");
}
It looks like a DOS, K&R hello-world. (They would have been running it on UNIX, of course).

You will find that many aspects of the Linux architecture are *like* those of DOS, only ten times better. Why? Linux is a flavor of UNIX, more than one distribution of which has achieved POSIX certification. DOS is a UNIX rip-off that was never very good at anything, let alone presenting an elegant architecture.

But regardless of the history of DOS and certain Redmond-area developers borrowing UNIX source, you've got an advantage if you're experienced with DOS and are moving to Linux. You will find a few cognates. The ANSI C/C++ libraries, as one would expect, are also the same, which is another plus.


Compiling.

By convention, the C compiler on UNIX platforms is called cc (good name). Even if a compiler named cc isn't installed, cc will almost always be a link to the default C compiler used on the system, so if you find yourself on a strange computer with no hope of compiling, never fear; cc is near.

That being the case, you should also know that gcc, or the GNU C Compiler, is the most popular C/C++ compiler on the Linux platform. It does a damn good job. There is a good chance that you have used gcc before even if you've never done Linux development, in the form of the time-honored DJGPP. So, it is safe to assume for the purpose of this article that you have gcc. If you don't, fine. Any C compiler will do for now. I will be using gcc, so bear with me.

As with any compiler, gcc has a billion command-line options. When you need something weird to happen, you will find the manual page listing for gcc options to be your best friend (type "man gcc" to read it).

You can control anything at any phase of the compilation with command-line options. For the moment, though, you only need to know a few things.

By convention, compilers on the UNIX platform by default output compiled, assembled source to the file a.out. If I were to call gcc with only the source file that I sought to have compiled on the command line, it would execute and produce a binary called a.out.

For example:

gcc hello.c
./a.out

Would execute our sample program. This is acceptable for quickies, but not for much else. The -o command-line option, followed by a file, will tell gcc to force any output into a certain file.

gcc -o hello hello.c would compile my previous program and place it in the binary hello, located in the working directory.


Analysis.

The entry-point for Linux programs is main(). The C standard libraries that you know so well are all available, as gcc is or can be an ANSI-compliant C compiler. The marvel of gcc is that while it LIKES ANSI, unless you tell it not to, it will tolerate things that shouldn't be tolerated, like unscoped class member function pointers being filled with member functions ::points at Mithrandir::.

Let's pick up the pace.


Program 2.

#include <unistd.h>

#define STDIN 0
#define STDOUT 1
#define STDERR 2

int main(int argc, char **argv) {
	char buffer[256];
	read(STDIN, &buffer, 10);
	write(STDOUT, &buffer, 10);
	if (argc!=2) {
   	write(STDERR, "Crashed!  YE GADS!", 18);
   	return 1;
	}
	else {
   	return 0;
	}
}
Compiling.

gcc -o unixio1 unixio1.c

What say we enjoy some more gcc command-line options?

Have you ever wanted the clean-cut assembly language of a program or a portion of a program compiled under MSVC++ or your other favorite Windows compiler? You probably had to go to great lengths, perhaps tricking the debugger or disassembling the binary.

Not so with any respectable UNIX compiler! UNIX compilers are a bit more oriented toward their purpose in life--they compile things! Whereas on Windows the code generator is often expected to produce assembled source, the code generator for a UNIX compiler produces assembly language. Period.

gcc is smart enough to know that you usually want the binary, not the assembly language source. So, by default it cooperates with it's sidekick assembler, GAS (GNU Assembler) to produce a binary for you. But, you can always write assembly-language source yourself and submit it to GAS directly. Or, you can tell GCC to halt the compilation process before it calls upon GAS with the -S option.

Check it out:

gcc -S -o unixio1.s unixio1.c

Open unixio1.s with your favorite text-editor. Notice anything?

Well, coming from Wintel, I'm sure you probably do. First of all, it's obvious that gcc generates extremely clean, straightforward assembly. Second of all, it's obvious that gcc doesn't use the conventional Intel assembly language syntax. GAS was written to support the AT&T assembly-language mnemonic syntax. This syntax is easier for a compiler to generate and easier for an assembler to assemble, but it may be harder for a programmer to write at first. I suggest that you don't worry about it at the moment, but do keep it in mind if you plan to write assembly or inline assembly source.


Analysis.

UNIX implements the same three parameters of the main-entry-point function main() that DOS does. The first parameter, argc, tells you how many command line arguments were given. The second parameter, char **argv, is an array of pointers to these arguments. The third parameter, not specified here, is an array of pointers to the system environment variables. These environment variables play a much more important role in system function under UNIX than under DOS. To see what I mean, type "set | less". Typically you will find two to three pages of environment variables.

Linux and other UNIX flavors also have a much more elegant I/O scheme than DOS. DOS *tried* to support elegant I/O, but failed rather miserably.

The read() and write() functions read from and write to, respectively, file descriptors. There are three standard file descriptors recognized everywhere in the operating system: STDIN, STDOUT, and STDERR. These descriptors represent the input, whatever it may be, the output, whatever that may be, and the error output, which is actually implemented, for the program.

This program is designed to read 10 characters from standard input and then write 10 characters to standard output. You can change where the standard input, output, and error go on the command-line in all available Linux shells. For instance, run the previous program with unixio1 > out. Then try running it with unixio1 < out.

Incidentally, I have also demonstrated a certain class of what is called a buffer overflow, the most stupid to write inadvertently and the easiest to exploit. Perhaps I am being vague. Let me demonstrate. Run the program and type 1234567890cd /etc;cat passwd | less. Unless you have shadowing on, you would be looking at portions of the password file you normally don't want to be visible. If this were running on your web-server under root as a CGI, you'd be pretty much dead if I came along and decided to modify rhosts for you, now wouldn't you? The moral of the story? The integrity of your own code can be a bigger security factor than attacks, if those sorts of things are ever considerations for you.


Program 3.

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/in.h>
#include <netdb.h>
#include <string.h>

#define FINGER_PORT 79
#define BUFFER_SIZE 1024

int main(int argc, char **argv) {
	char *host, *user;
	char buffer[BUFFER_SIZE];
	struct sockaddr_in remotehost;
	struct in_addr addr;
	struct hostent *H = NULL;
	int s, index;

	if (argc != 3) {
   	printf("Usage: thumb user host\n\n");
   	return 0;
	}

	memset(&addr, 0, sizeof(addr));
	memset(buffer, 0, sizeof(buffer));

	user = argv[1];
	host = argv[2];

	s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (inet_aton(host, &addr)) {
   	H = gethostbyaddr((char *)&addr, sizeof(addr), AF_INET);
	}

	if (!H) {
   	H = gethostbyname(host);
   	if (!H) {
      	printf("Unable to resolve host.\n\n");
      	close(s);
      	return 1;
   	}
   	memcpy(&addr, H->h_addr, sizeof(struct in_addr));
   }

   printf("Fingering %s on %s (%s)...\n", user, H->h_name, inet_ntoa(addr));

   memset(&remotehost, 0, sizeof(remotehost));
   memcpy(&remotehost.sin_addr.s_addr, &addr, sizeof(struct in_addr));
   remotehost.sin_port = htons(FINGER_PORT);

   if (connect(s, &remotehost, sizeof(remotehost)) == -1) {
  	printf("Unable to connect to finger port on host.\n\n");
  	close(s);
  	return 1;
   }

   send(s, user, strlen(user), 0);
   send(s, "\n", sizeof("\n"), 0);
   recv(s, buffer, sizeof(buffer), 0);
   close(s);

   for(index=0;index<sizeof(buffer);index++) {
  	if (!buffer[index])
     	break;
  	printf("%c", buffer[index]);
   }

   return 0;
}
Compiling.

gcc -o thumb thumb.c

I once found myself in a position where I had about 4,000 lines of a graphics engine done and it worked beautifully. Then I hacked out a 250 line scripting engine and it didn't work. I searched *days* for the bug that was giving me grief...finally, in an act of desperation, I proceeded to read through the graphics engine and left the scripting engine alone for a while. As it turns out, I was allocating about 100k less than I should have for the double buffer...

I didn't notice the problem because it was not very obvious. The moral of the story? If it just won't work and you've checked all the contingencies, then you probably made a lame mistake. When I wrote the pretty simple finger client above, I experienced a frustrating dilemma that took almost an hour to fix (don't laugh). One of the error conditions--in which the host could not be resolved--kept causing segmentation faults (a reference to memory outside of the program's data segment was made; Linux implements far more stringent memory protection than either Win32 or DOS). I rearranged the code, checked my buffers, and even looked to see if there were known bugs with DNS resolution under the Linux implementation of BSD sockets.

As it turns out, I included a reference to a string that didn't exist in the error message...or in other words, the error message was causing the error...

Which brings us to our topic. Okay, not really, but it was a good story. ;)

Occasionally, you will find that you need to check up on your macros, particularly in large projects with many files. Even when reading source, it can be quite confusing to step through all the macro definitions that someone else has established for their code. The pre-processor expands these, so why bother trying to figure out the macro'd up code? Use the -E option to tell gcc to run its pre-processor, then dump the output. You can use the -C option in conjunction with this option to keep comments in (they are almost always pre-processed out in compilers written by sane people, but I have seen at least one rather sadistic fellow try to parse them...)

gcc -E -C -o thumb.i thumb.c

More rarely, you may find that you have source that you do not WANT gcc to pre-process. In this case, simply change the extension of the source file to .i (for C, or .ii for C++), and gcc will obey. Pretty cool, eh?


Analysis.

There's a lot here, but nothing that I haven't written about before. This program demonstrates the BSD sockets functionality in UNIX, and, if you looked closely, more about Linux's elegant I/O. If there is a function that you have no clue about, you're in luck. Your distribution probably came with the programming manual pages--all systems functions are documented in the manual pages. Usually, there's a good chance you can find info on a function foo() with 'man foo', but sometimes foo() may also be the name of a utility. If that's the case, figure out man or use a better tool like xman under X. ;)

The only really interesting thing about this finger client is the way it closes the socket. Note that the system I/O call close(), which takes a file descriptor, can also be used to close a socket. Indeed, what is really so neat about UNIX is that sockets, devices, and files all share a common pool of file descriptors. You will learn that one can also open a device for I/O by opening a file mapped to it.

Because I/O is so universal, you may wonder whether the previously discussed read() and write() functions can be used on any file descriptor. Of course! I can send to a socket with a filesystem write() call as well as a send() or sendto() call. That's something that Win32 pulled off only in WinSock 2 after BSDites complained like hell about porting their programs that relied on this kind of I/O interoperability. Tell me that's not cool.

Also, you may now be starting to wonder about system calls, what exactly they are, and when exactly they must be used. In a flavor of UNIX, system calls are *the* mechanism for communicating with the kernel, which acts on behalf of all user processes to handle I/O and other system-layer functions. Linux is very serious about a four ring memory protection model; if you're in the user ring, privileged instructions do not get executed unless you tell the kernel or move to another protection level. That's the deal.

The standard library for the compiler that you're using does a pretty complete job of interfacing to system calls as necessary, and to remain as portable as possible, you should rely on standard library calls, not the system call interface. ioctl() and other low-level system calls are very platform-specific. Relying heavily (outside of modules, that is) on them in code that you want to run on DOS or even another flavor of UNIX is a very bad move.


Program 4.

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

main(int argc, char **argv, char **envp) {
	pid_t me, a;

	me = fork();
	if (!me) {
   	printf("Child process about to execve() a shell...\n");
   	printf("Type 'exit' to leave\n");
   	if (execve("/bin/bash", argv, envp)) {
      	printf("Error condition %d.\n", errno);
   	}
	else {
   	a = waitpid(me, NULL, 0);
   	printf("Parent process done.\n");
	}
}
Compiling.

gcc -o process process.c


Analysis.

Contemporary UNIX flavors implement multi-threading--heavily. Because it is so central to UNIX architecture, the POSIX standard specifies it. Although there are a lot of details related to multi-threading that are far too unimportant for the sake of getting started, it is important that you have a sound understanding of how process management works in UNIX.

The kernel representation of a program in UNIX is a process. For all intents and purposes, a process is simply a set of structures and data uniquely identified by an integer called a Process Identifier (PID). The kernel stores (within its own address-space) process structures indexed by PID's that point to process memory images. These images are composed of three separate segments: a machine image (binary code) for the process, called the text segment; the data segment, made up of all dynamic memory resources the process uses; and the stack segment, where static items are stored and the running program stack is kept. These segments are strictly protected (overflows can happen, resulting in segmentation faults). It is possible for any of a processes segments to be shared with another process in Linux.

The fork() system call creates a new process (said to be a child) and copies the parent process's text, data, and stack segments to it precisely. It creates a child that is an exact copy of the parent save for the pid. In the parent process thread of the program, fork() returns the PID of the child. In the child process thread, fork() returns 0. You can use this return value to select which of two blocks of code to execute, as in the example above.

vfork(), on the other hand, is supposed to cause the child to share the parent's resources rather than mirroring them. This does not happen in Linux for reasons that we will not venture to explore, but it is an important performance issue in some flavors.

clone() is an extended rendition of fork() with no options. In reality, fork() is implemented as a front-end to clone() in Linux. See the clone man pages for more information on this.

Once a child process has been forked, pretty much anything can be done. Sometimes forking can be useful for multi-threaded applications, while other times it is used to temporarily "shell" to other applications. Any process can call execve() or one of the exec..() line of functions that act as front-ends to it; the execve() function causes the binary of the target program to be loaded in the calling process's address space. In essence, the calling process becomes an instance of the specified program. Often, it is useful to fork a child then execve(), or fork and exec; this is actually done at login and by shells.

The wait(), waitpid(), wait3(), and wait4() system calls all do about the same thing--they wait for a child process to terminate before continuing and give information about its termination. This is obviously useful for the purpose of synchronization.

Okay, I can't resist. The rest of this is just too cool. I HAVE to talk about how Linux boots.

After a PC runs through its power-on/self-test routines and goes to look for the OS boot loader in the first 512 bytes of the first boot device, it finds LILO or some other cute little boot manager that lets you run more than one OS on the same machine. You select Linux and LILO transfers execution to a portion of the kernel designed to read the kernel memory image (ever seen "reading vmlinuz..."?) From there, the kernel begins to initialize itself and the machine. It loads most modules and creates a single process with PID 1 and stores the image of a binary called init in its text segment. init reads instructions from /etc/inittab and follows them to boot the system. Usually, inittab has init run a set of scripts in the /etc/rc.d directory and then fork terminal control processes (getty). getty, in turn, waits for a user to respond at the login prompt and then forks a login process, which starts a shell for you. When you execute a utility, (i.e., most things you would call shell commands), your shell forks and execs them.


Program 5.

#include <time.h>
#include <sys/time.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>

void alrm(int sig) {
 	printf("\nALARM!!\n");
 	kill(getpid(), SIGKILL);
}

main() {
	struct tm *local;
	struct itimerval it, old;
	struct sigaction sa;
	time_t t;
	int s, r;

	time(&t);
	local = localtime(&t);
	memset(&it, 0, sizeof(it));
	memset(&old, 0, sizeof(old));

	printf("\nIt is %2d:%2d:%2d now.\n", local->tm_hour, local->tm_min, local->tm_sec);
	printf("For how many seconds should the alarm be dormant?\n");
	scanf("%d", &s);
	printf("\n\n");

	it.it_value.tv_sec = s;
	it.it_value.tv_usec = 1000 * s;

	r = setitimer(ITIMER_REAL, &it, &old);
	if (r == EFAULT || r == EINVAL) {
   	printf("Unable to set interval timer.\n");
   	return;
	}

	memset(&sa, 0, sizeof(sa));

	sa.sa_handler = &alarm;
	sa.sa_flags = SA_ONESHOT;

	sigaction(SIGALRM, &sa, NULL);
	
	while (1){};
}
Analysis.

Message passing is the central means of controlling Win32 applications, and indeed of controlling Win32 in general. Don't let anybody lead you to believe this idea was new or even remotely introduced by the Windows platform. As much as a generation before Win32, the seed was planted for message passing in UNIX--only it wasn't planted intentionally.

UNIX provides kernel-level process management functions called signals that were originally designed to be raised on errors. Signals are quite powerful..they control everything to do with process management and exceptional error conditions. A process that is stuck in an infinite loop can always be terminated with SIGKILL, as is demonstrated above. SIGINT (although not specified by POSIX) can be sent to most processes on most flavors of UNIX to break out of them (ctrl+c).

These things are possible because signals are, by default, all handled in a standard way by the kernel.

However, for all but a few important signals, it is possible to replace the default signal handler or simply to add one in the first place. The replacement handler is unique for a process. In fact, you can even go so far as to define new signals, identified by SIGUSR1 and SIGUSR2 (POSIX) or in Linux any signals greater than or equal to the identifier _NSIG.

Signals have become extremely popular on UNIX for job control and even windowing systems (BSD in particular uses them heavily).

It is possible to send any signal to any process via the kill system call, also implemented as a system utility. The name is a bit misleading, granted, but it gets the job done. You will find kill most useful on the command-line, especially if you do a lot of service work (most inet servers will require you to send a certain signal in order for them to reread configuration or restart).

Also demonstrated here are built-in interval timers. These are more fun, standard things. Check out the man pages for setitimer/getitimer for more reading on these.


End of Article 1.

Welp, that's all for now, folks. Jered is tired. Remember...mixed signals can fork your processes.





Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS