Odd issue when using puts() implementation

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Post Reply
avcado
Member
Member
Posts: 33
Joined: Wed Jan 20, 2021 11:32 am
GitHub: https://github.com/mxtlrr/

Odd issue when using puts() implementation

Post by avcado »

I'm in the process of switching a codebase of mine from GRUB to a custom bootloader so I can have more control over the booting process. Anyways, I got kernel loading working, and when I tried to actually print something, nothing showed up!

Here's the bootloader code:
stage0.asm

Code: Select all

[org 0x7c00]
[bits 16]
xor ax, ax
mov ds, ax
mov es, ax

mov sp, 0x7c00
mov bp, sp

mov ax, 0x0003
int 0x10

; BIOS should put boot drive in dl
mov [BOOT_DRIVE], dl

; mov ax, 0x020F        ;; 02 = read, 0F = sectors
mov ah, 02h
mov al, 32
mov bx, 0x7e00
mov dl, [BOOT_DRIVE]
mov cx, 0x0002
mov dh, 0x00
int 0x13
jc Failed

cli
lgdt [GDT_descriptor]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 0x08:0x7e00 ;; !!

jmp $
BOOT_DRIVE: db 0

GDT_start:
  GDT_null:
    dd 0x0
    dd 0x0

  GDT_code:
    dw 0xffff
    dw 0x0
    db 0x0
    db 0b10011010
    db 0b11001111
    db 0x0

  GDT_data:
    dw 0xffff
    dw 0x0
    db 0x0
    db 0b10010010
    db 0b11001111
    db 0x0

GDT_end:

GDT_descriptor:
  dw GDT_end - GDT_start - 1
  dd GDT_start

Failed:
  mov ax, 0x0e5a
  int 0x10
  jmp $

times 510-($-$$) db 0
dw 0xaa55
stage1.asm

Code: Select all

[extern _start]
[bits 32]

;; Setup stack
mov ax, 0x10
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax

mov ebp, 0x9000
mov esp, ebp

call _start
jmp $
Here's the kernel code:

Code: Select all

volatile uint16_t* vga_buffer = (uint16_t*)0xB8000;
int x_pos = 0; int y_pos = 0;
uint8_t color = VGA_COLOR_WHITE;

void putc(unsigned char c){
  uint16_t attrib = (0 << 4) | (color & 0x0F);
  volatile uint16_t * where;
  where = (volatile uint16_t *)0xB8000 + (y_pos * 80 + x_pos);
  *where = c | (attrib << 8);
	if(++x_pos >= 80){
		x_pos = 0;
		y_pos++;
	
	}
}

void set_color(uint8_t col){
	color = col;
}

void puts(char* str){
	while(*str){
		putc(*str);
		*str++;
	}
}

void _start(){
	set_color(0x2);
	puts("Hello World");
}
I link like this:

Code: Select all

OUTPUT_FORMAT(binary)
ENTRY(_start)

SECTIONS
{
  . = 0x7e00;

  .text : ALIGN(0x1000)
  {
    *(.text)
  }

  .data : ALIGN(0x1000)
  {
    *(.data)
  }

  .rodata : ALIGN(0x1000)
  {
    *(.rodata)
  }

  .bss : ALIGN(0x1000)
  {
    *(COMMON)
    *(.bss)
  }
}
and then concatenate the stage1 output with the output from the linker.


I should note that doing the following things work:

Code: Select all

putc('H');

Code: Select all

char str[] = "Hello";
for(int i = 0; i < str[i] != '\0'; i++){
	putc(str[i]);
}

Code: Select all

char str[] = "Hello";
puts(str);
What's going on here? I read that it has something to do with .rodata not being linked, but if that was true, then set_color wouldn't work -- but it does. Thanks in advance.
nullplan
Member
Member
Posts: 1840
Joined: Wed Aug 30, 2017 8:24 am

Re: Odd issue when using puts() implementation

Post by nullplan »

avcado wrote: Sat Feb 15, 2025 12:14 pm I'm in the process of switching a codebase of mine from GRUB to a custom bootloader so I can have more control over the booting process.
I would strongly counsel against such a thing. Legacy PC is a miserable platform to code against, and programs like GRUB go to a lot of effort to standardize the environment for transfer to your protected-mode kernel. They've already done the work so you don't have to.
avcado wrote: Sat Feb 15, 2025 12:14 pm What's going on here? I read that it has something to do with .rodata not being linked, but if that was true, then set_color wouldn't work -- but it does. Thanks in advance.
How do you figure? set_color() does not reference the .rodata section. In your linker script I see these huge alignment directives. No clue what they are supposed to do there. Why do you align the .text section to 4k? If that is effective, then your .text section will show up at 0x8000, not 0x7e00, and your .rodata section will be at 0x9000. You also set your boot stack to 0x9000, and with the stack growing downwards this means your stack will eventually overwrite your code.

So my guess is that your linker actually links everything to run starting from address 0x8000 due to your alignment directives, and you loading the kernel to 0x7e00 will just not work. And for the short string literals you put on stack, GCC likely just generated the code to turn them into integer literals pasted to the stack. You can confirm both of these suspicions by just disassembling the resulting binary to see what the output actually is. So I guess you'd have the same issue if you replaced your simple "Hello" with the Yorick speech from Hamlet.

Things like this are what using GRUB, a competent linker script, and a sensible binary format would save you from.
Carpe diem!
avcado
Member
Member
Posts: 33
Joined: Wed Jan 20, 2021 11:32 am
GitHub: https://github.com/mxtlrr/

Re: Odd issue when using puts() implementation

Post by avcado »

nullplan wrote: Sat Feb 15, 2025 2:23 pm I would strongly counsel against such a thing. Legacy PC is a miserable platform to code against, and programs like GRUB go to a lot of effort to standardize the environment for transfer to your protected-mode kernel. They've already done the work so you don't have to.
I understand. I'm writing software that targets mainly old machines (like 8088s, 286s, etc), and I'd like to have more control over the booting process, namely:
  • Detect if the CPU is 8088 and bail out / do something else.
  • Use Int 1A/AX=0xB101 to detect PCI, and do something in case there's no PCI
nullplan wrote: Sat Feb 15, 2025 2:23 pm Things like this are what using GRUB, a competent linker script, and a sensible binary format would save you from.
I agree. The linker script is kind of a mess, so I'll fix it (albeit I have a limited knowledge of how to write a linker script).
Projects: DXB
nullplan
Member
Member
Posts: 1840
Joined: Wed Aug 30, 2017 8:24 am

Re: Odd issue when using puts() implementation

Post by nullplan »

avcado wrote: Sat Feb 15, 2025 6:10 pm I understand. I'm writing software that targets mainly old machines (like 8088s, 286s, etc), and I'd like to have more control over the booting process, namely:
  • Detect if the CPU is 8088 and bail out / do something else.
  • Use Int 1A/AX=0xB101 to detect PCI, and do something in case there's no PCI
I'm not sure what the benefit of the former is. You get a nicer error message, but the end effect remains the same: The OS isn't booting.

But if you want to detect an 8088, you could just use "push sp". That instruction changed meaning in the 286, before that it would push the new value of SP, and after it would push the old value of SP. So you get

Code: Select all

push sp
pop ax
cmp sp, ax
jne 8086_or_80186
I used that trick in my VBR to detect too-old CPUs. The second trick is that from the 186 onward, CPUs have had an undefined instruction interrupt, so I just make my "CPU too old" routine the handler for that and proceed as if the CPU was modern.

To detect PCI, the simplest is to first ensure you are on a 386 or later, and then write 0x80000000 to port 0xCF8 and see if you can read that value back. PCI BIOS is one of those things I never quite understood, because PCI is so standardized.
avcado wrote: Sat Feb 15, 2025 6:10 pm I agree. The linker script is kind of a mess, so I'll fix it (albeit I have a limited knowledge of how to write a linker script).
Specific areas for improvement in the script:
  • Get rid of the alignment specifiers. They make no sense in this amount.
  • Handle sections with trailing parts to their name, so e.g. make the .text section into

    Code: Select all

    .text : {
      *(.text)
      *(.text.*)
    }
    The compiler can generate multiple sections that start with the same name, and the linker then only has a heuristic where to put them A heuristic that works most of the time but you can get rid of the ambiguity.
Alternatively, you could get rid of it, just link with -Ttext=0x7e00 and jump to the entry point given in the ELF header. Because another problem you have is that you aren't placing your startup code in a special section at the start of the file, so the linker can just place it anywhere.
Carpe diem!
Octocontrabass
Member
Member
Posts: 5695
Joined: Mon Mar 25, 2013 7:01 pm

Re: Odd issue when using puts() implementation

Post by Octocontrabass »

nullplan wrote: Sat Feb 15, 2025 11:26 pmTo detect PCI, the simplest is to first ensure you are on a 386 or later, and then write 0x80000000 to port 0xCF8 and see if you can read that value back. PCI BIOS is one of those things I never quite understood, because PCI is so standardized.
The first PCI chipsets didn't have port 0xCF8, so your method will fail to detect PCI on those PCs. Older versions of the PCI standard offered two options for implementing configuration space access, and those chipsets happened to use the option that became obsolete.

Blindly writing to port 0xCF8 can also cause problems with the FPU on chipsets that predate PCI. Legacy PC is, indeed, a miserable platform.
nullplan wrote: Sat Feb 15, 2025 11:26 pmBecause another problem you have is that you aren't placing your startup code in a special section at the start of the file, so the linker can just place it anywhere.
I agree that it's a bad idea to rely on it, but binutils promises it will only be a problem if you change the order of the input files: input sections are copied to the output in the order ld finds them.
Post Reply