This tutorial is irrelative to operating system, but these functions are widely used in our later tutorials. In general, they are used as substitutions of C standard library functions.
If you are not interested in implementations of these functions, just note kprintf
works likes printf
in C and print_c
prints one character on screen with given background and foreground colors.
It is safe for you to skip this tutorial.
printf is a programmer's good friend, so for printing strings, numbers etc on screen, I am going to implement a function works like printf
for output instead of messing with B8000.
I'm not going to implement a fully functioning printf
, for Skelix we just need it to print strings, numbers in hexadecimal or binary format, positive integers and characters.
The most important feature is it must be able to accept variable length arguments.
By default, when a function like func(
is called, its
assembly likes this,int
arg1, int
arg2, int
arg3)
pushl arg3
pushl arg2
pushl arg1
call func
Arguments are pushed one by one from right to left in arguments list, so if we want to get more arguments then just dig the stack deeper. How can we know how many arguments are there?
The answer is how many %X
stuff in format string, how many arguments we have to parse.
In 32-bit mode, all date types which byte length smaller than a integer will be pushed as an integer. That implies even a one byte character occupies 4 bytes in stack We have to be sure we parse the argument correctly.
We are going to let kprintf
accept arguments in this format kprintf(color, format string, arguments...)
,
the first argument defines what fg/bg color combination for output. There are some macros we are going to use for parsing stack, if you are familiar with C, then you must be familiar with these macros.
03/kprintf.c
#define args_list char
*
This macro used in kprintf
to convert the stack space to a character stream for further processing.
#define _arg_stack_size(type) \
(((
(type)-1)/sizeof
(sizeof
)+1)*int
(sizeof
))int
The macro up rounds the argument size to calculate how many 4-byte it occupies in stack.
#define args_start(ap, fmt) do
{ \
ap = (char
*)((unsigned
int
)&fmt + _arg_stack_size(&fmt)); \
} while
(0)
Arguments are after format string, or I should say on the top of fmt
in stack, this macro gets the starting address of those arguments.
#define args_end(ap)
Doesn't do anything at this moment
#define args_next(ap, type) (((type *)(ap+=_arg_stack_size(type)))[-1])
Gets the address of next argument.
static
char
buf[1024] = {-1};
static
int
ptr = -1;
We put all characters are going to be printed into a buffer and make a pointer point to the next available place.
/* valid base: 2, 8, 10 */
static
void
parse_num(unsigned
value, int
unsigned
base) {int
unsigned
int
n = value / base;
int
r = value % base;
if
(r < 0) {
r += base;
--n;
}
if
(value >= base)
parse_num(n, base);
buf[ptr++] = "0123456789"[r];
}
static
void
parse_hex(unsigned
int
value) {
int
i = 8;
while
(i-- > 0) {
buf[ptr++] = "0123456789abcdef"[(value>>(i*4))&0xf];
}
}
These two functions parse the given value into different radix.
/* %s, %c, %x, %d, %% */
void
kprintf(enum
KP_LEVEL kl, const
char
*fmt, ...) {
int
i = 0;
char
*s;
/* must be the same size as enum
KP_LEVEL */
struct
KPC_STRUCT {
COLOUR fg;
COLOUR bg;
} KPL[] = {
{BRIGHT_WHITE, BLACK},
{YELLOW, RED},
};
is defined in enum
KP_LEVEL {KPL_DUMP, KPL_PANIC}03/include/kprintf.h
, it inicates two color scheme for output,
KPL_DUMP
prints strings with bright white foreground and black background, KPL_PANIC
prints strings with yellow foreground and red background.
Those color constants are defined in 03/include/scr.h
, they are going to be introduced later.
args_list args;
args_start(args, fmt);
ptr = 0;
for
(; fmt[i]; ++i) {
if
((fmt[i]!='%') && (fmt[i]!='\\')) {
buf[ptr++] = fmt[i];
continue
;
} else
if
(fmt[i] == '\\') {
/* \a \b \t \n \v \f \r \\ */
switch
(fmt[++i]) {
case
'a': buf[ptr++] = '\a'; break
;
case
'b': buf[ptr++] = '\b'; break
;
case
't': buf[ptr++] = '\t'; break
;
case
'n': buf[ptr++] = '\n'; break
;
case
'r': buf[ptr++] = '\r'; break
;
case
'\\':buf[ptr++] = '\\'; break
;
}
continue
;
}
We accept those escape sequence like printf
.
/* fmt[i] == '%' */
switch
(fmt[++i]) {
case
's':
s = (
*)args_next(args, char
*);char
while
(*s)
buf[ptr++] = *s++;
break
;
case
'c':
/* why is int
?? */
buf[ptr++] = (char
)args_next(args, int
);
break
;
case
'x':
parse_hex((unsigned
)args_next(args, long
unsigned
));long
break
;
case
'd':
parse_num((unsigned
)args_next(args, long
unsigned
), 10);long
break
;
case
'%':
buf[ptr++] = '%';
break
;
default
:
buf[ptr++] = fmt[i];
break
;
}
}
buf[ptr] = '\0';
Puts an trailing \0 in buffer.
args_end(args);
for
(i=0; i<ptr; ++i)
print_c(buf[i], KPL[kl].fg, KPL[kl].bg);
}
Prints all characters in buffer on screen with function print_c
.
Before we go any further, there is a problem might occur depends on you complier version. Even with -nostdlib
option, based on GCC manual,
"The compiler may generate calls to memcmp
, memset
and memcpy
for System V (and ISO C) environments or to bcopy
and bzero
for BSD environment."
GCC might give you some "can't find memcpy
" error etc. I didn't have this problem before when I was using my old version GCC, but it happens now.
Wee have to implement these functions by ourselves.
/* result is currect, even when both area overlap */
void
bcopy(const
*src, void
*dest, void
unsigned
int
n) {
const
*s = (char
const
*)src;char
*d = (char
*)dest;char
if
(s <= d)
for
(; n>0; --n)
d[n-1] = s[n-1];
else
for
(; n>0; --n)
*d++ = *s++;
}
void
bzero(void
*dest, unsigned
int
n) {
memset(dest, 0, n);
}
void
*
memcpy(
*dest, void
const
*src, void
unsigned
int
n) {
bcopy(src, dest, n);
return
dest;
}
void
*
memset(void
*dest,
c, int
unsigned
n) {int
*d = (char
*)dest;char
for
(; n>0; --n)
*d++ = (char
)c;
return
dest;
}
int
memcmp(const
*s1, void
const
*s2, void
unsigned
int
n) {
const
*s3 = (char
const
*)s1;char
const
*s4 = (char
const
*)s2;char
for
(; n>0; --n) {
if
(*s3 > *s4)
return
1;
else
if
(*s3 < *s4)
return
-1;
++s3;
++s4;
}
return
0;
}
int
strcmp(const
*s1, char
const
*s2) {char
while
(*s1 && *s2) {
int
r = *s1++ - *s2++;
if
(r)
return
r;
}
return
(*s1)?1:-1;
}
char
*
strcpy(
*dest, char
const
*src) {char
char
*p = dest;
while
( (*dest++ = *src++))
;
*dest = 0;
return
p;
}
unsigned
int
strlen(const
char
*s) {
unsigned
int
n = 0;
while
(*s++)
++n;
return
n;
}
Manipulating video memory directly is messy, so we need a module to handle screen output. We need to define some contants as usual.
03/include/scr.h
#define MAX_LINES 25
#define MAX_COLUMNS 80
By default we have a 80x25 screen.
#define TAB_WIDTH 8 /* must be power of 2 */
/* color text mode, the video ram starts from 0xb8000,
we all have color text mode, right? :) */
#define VIDEO_RAM 0xb8000
I presume we are all in color text mode, at this mode the adapter uses 0xB8000-0xBF000 as video RAM. In general it works in 80 rows, 25 columns and 16 colors. This memory space is divided into multiple video pages of 4KB each. We can use all pages at the same time (here is a program I wrote before uses multiple display pages), but only one page is visible. To display a single character, two bytes are used, that is the character byte and the attribute byte. The character byte contains the value of the character. The attribute byte is defined like this,
Bit 7 | Blinking |
Bits 6-4 | Background color |
Bit 3 | Bright |
Bit3 2-0 | Foreground color |
This table will be used shortly.
#define LINE_RAM (MAX_COLUMNS*2)
#define PAGE_RAM (MAX_LINE*MAX_COLUMNS)
#define BLANK_CHAR (' ')
#define BLANK_ATTR (0x70) /* white fg, black bg */ //#Bug 002
#define CHAR_OFF(x,y) (LINE_RAM*(y)+2*(x))
Calculates the offset of a given x
, y
.
typedef
enum
COLOUR_TAG {
BLACK, BLUE, GREEN, CYAN, RED, MAGENTA, BROWN, WHITE,
GRAY, LIGHT_BLUE, LIGHT_GREEN, LIGHT_CYAN,
LIGHT_RED, LIGHT_MAGENTA, YELLOW, BRIGHT_WHITE
} COLOUR;
Bases on the table given above, defines these colour tags.
03/scr.c
static
int
csr_x = 0;
static
int
csr_y = 0;
Because we are going to use just one video page, so cursor postion is stored in csr_x
and csr_y
globally.
static
void
scroll(int
lines) {
This function takes the number of lines we want to scroll down as the argument, it just overwrite some memory to make it looks like scroll down.
*p = (short
*)(VIDEO_RAM+CHAR_OFF(MAX_COLUMNS-1, MAX_LINES-1));short
int
i = MAX_COLUMNS-1;
memcpy((
*)VIDEO_RAM, (void
*)(VIDEO_RAM+LINE_RAM*lines),void
LINE_RAM*(MAX_LINES-lines));
for
(; i>=0; --i)
*p-- = (short
)((BLANK_ATTR<<4)|BLANK_CHAR); //#Bug 002
}
Clears all characters in bottom lines to make it looks like whole screen scrolls up. (#Bug 002 Song Jiang pointed out another bug, BLANK_ATTR
should be
shifted 8 bits instead of 4. By fixing this bug, I found I made a mistake on value BLANK_ATTR
, it should be 0x07 instead of 0x70. Another problem is this scroll
function could just scroll one line originally, search #Bug 002 in this page to find the related code change).
void
set_cursor(
x, int
y) {int
This function sets the position of cursor.
csr_x = x;
csr_y = y;
Setting the position of cursor could be a race condition, but print_c
is used in kernel only, so I did not close all interrupts.
It might cause some problem which I have not discovered.
outb(0x0e, 0x3d4);
outb(((csr_x+csr_y*MAX_COLUMNS)>>8)&0xff, 0x3d5);
outb(0x0f, 0x3d4);
outb(((csr_x+csr_y*MAX_COLUMNS))&0xff, 0x3d5);
Sets the cursor position via port 0x3D4, 0x3D5.
}
void
print_c(char
c, COLOUR fg, COLOUR bg) {
We use this function to print one character on current cursor position on screen.
char
*p;
char
attr;
p = (char
*)VIDEO_RAM+CHAR_OFF(csr_x, csr_y);
Gets the current cursor postion in memory.
attr = (char
)(bg<<4|fg);
switch
(c) {
case
'\r':
csr_x = 0;
break
;
case
'\n':
for
(; csr_x<MAX_COLUMNS; ++csr_x) {
*p++ = BLANK_CHAR;
*p++ = attr;
}
break
;
case
'\t':
c = csr_x+TAB_WIDTH-(csr_x&(TAB_WIDTH-1));
c = c<MAX_COLUMNS?c:MAX_COLUMNS;
for
(; csr_x<c; ++csr_x) {
*p++ = BLANK_CHAR;
*p++ = attr;
}
break
;
case
'\b':
if
((! csr_x) && (! csr_y))
return
;
if
(! csr_x) {
csr_x = MAX_COLUMNS - 1;
--csr_y;
} else
--csr_x;
((
*)p)[-1] = (short
)((BLANK_ATTR<<4)|BLANK_CHAR);short
break
;
default
:
*p++ = c;
*p++ = attr;
++csr_x;
break
;
}
if
(csr_x >= MAX_COLUMNS) {
csr_x = 0;
if
(csr_y < MAX_LINES-1)
++csr_y;
else
scroll(1);
}
set_cursor(csr_x, csr_y);
Reset the cursor postion after printing.
}
Feel free to use my code. Please contact me if you have any questions.