Author Topic: How to link ld files segments accordingly for NASM programs  (Read 12182 times)

Offline Deskman243

  • Jr. Member
  • *
  • Posts: 49
Greetings everyone

Today I was designing builds for my program in the assembler hoping to get a verification for this subject. Right now I have a certain configuration file where my code runs seemingly well however if I try to change the address of one of the files I lose functionality(especially obvious as this file is a graphic component). I had used to use NASM's ORG instruction for most programs but I use ld for my configurations whenever the file is larger than 512b (this was the subject of my previous post).

To surmize I would like to share the design here as an illustration for the purpose of review. The main program is just a function of ordinary NASM and x86 instructions modeled for the 1 MB BIOS function platform and outlined simply by the files and figure below. One other quick observation to clarify is that most of the stage files correspond to the paths shown.

Boot => Stage1.asm (16-32 gate function) => Stage2.asm  (BIOS_16 code fitting) =>Stage3.asm (BIOS graphics functions)
+(secondary path test) test2.asm


The question is essentially a product of changing the aforementioned path by any means. In the build file you can observe that BOTH ld and ORG instructions however the fit for stage3 graphics is tight as is so I'm trying to move this to earlier segments that 0xA000. If I change this to anywhere other than that segment (0x9000,0x8000) I just get a blank screen. Naturally speaking I know I have to change a module in each file as outlined below.

Disk_Read(boot.asm)=>Jump_From_Gate(Stage1.asm)=>Jump_Into_BIOS16_Segment(Stage2.asm)=>Stage3_Addressment

From what I understand if it runs for one address it should run for any address but it doesn't.  I know that there's a difference between the two file types but the inferences here look kinda conspicuous as is. That's why I was very curious and hoping here if anyone else understood what happened.I'm posting the full source and I'd be really happy and available to correspond any further. One more thing was I had to add stage3 attach stage3 due to file constraints. Cheers!
 
boot.asm
Code: [Select]
;    ReTimerOS
;    Copyright (C) 2022,2023  Christopher Hoy
;
;    This file is part of ReTimerOS
;    ReTimerOS is free software: you can redistribute it and/or modify
;    it under the terms of the GNU General Public License as published by
;    the Free Software Foundation, either version 3 of the License, or
;    (at your option) any later version.
;
;    This program is distributed in the hope that it will be useful,
;    but WITHOUT ANY WARRANTY; without even the implied warranty of
;    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;    GNU General Public License for more details.
;
;    You should have received a copy of the GNU General Public License
;    along with this program.  If not, see <https://www.gnu.org/licenses/>.


[org 0x7c00]

[bits 16]

mov cx,0
mov ds,cx
mov es,cx


cld

mov si,msgString
call kprint

mov cx,0x1000
mov es,cx
mov bx,0x0000_0000
;mov bx,0x0000_D000
mov cl,5
mov bx,msgString
;mov bx,0x0000_D000
mov bx,0x0000_0000
mov cl,5
mov al,1
call read_section
;mov cx,0xD00
mov cx,0x1000
mov es,cx
mov bx,0
;mov al,1
mov cl,5
mov al,1
call read_section


mov cx,0
mov ds,cx
mov es,cx

;mov bx,0x0000_9000
mov bx,0x0000_A000
mov cl,4
mov bx,msgString
;mov bx,0x0000_9000
mov bx,0x0000_A000
mov cl,4
mov al,0x30
call read_section
;mov cx,0x900
mov cx,0xA00
mov es,cx
mov bx,0
;mov al,1
mov cl,4
;mov bx,0x0000_8000
mov al,0x30
call read_section

mov bx,0x0000_8000
mov cl,3
mov bx,msgString
mov bx,0x0000_8000
mov cl,3
mov al,1
call read_section
mov cx,0x800
mov es,cx
mov bx,0
mov cl,3
;mov bx,0x0000_8000
mov al,1
call read_section
mov bx,0x0000_7e00


mov bx,0x0000_7e00
mov cl,2
mov bx,msgString
mov bx,0x0000_7e00
mov cl,2
mov al,1
call read_section
mov cx,0x7e0
mov es,cx
mov bx,0
mov cl,2
;mov bx,0x0000_8000
mov al,1
call read_section
mov bx,0x0000_7e00

;mov cx,0
;mov es,cx


pusha

jmp 0x7e00
;jmp 0x9200
;jmp 0xD000




jmp $

kprint:
   cld
   mov ah,0x0E

kstring:
   lodsb
   int 0x10
   cmp al,0x00
   jnz kstring
   ret

read_section:
   
   mov ah,0x02
;   mov al,1
   mov ch,0
   mov dh,0
   int 0x13
   jc .disk_check

   ret

.disk_check:


   mov si,checkString
   call kprint
   jmp $
.done:
   ret


;section .data 

align 4
BOOT_DRIVE:   db 0
msgString:   db 'Game platform premier',10,13,0
checkString:   db 'Section did not load',10,13,0
checkString2:   db 'disk complete',10,13,0

times 510 -($-$$) db 0
dw 0xaa55
   



stage1.asm
Code: [Select]


;%define REBASE_ADDRESS(A)  (0x7E00 + ((A) - protectedGate1))

%define BUILD_GDT_DESC(bounds,base,access,flags) \
((( base & 0x00FFFFFF) << 16) | \
(( base & 0xFF000000) << 32) | \
( bounds & 0x0000FFFF) |       \
(( bounds & 0x000F0000) << 32) | \
(( access & 0xFF) << 40) |       \
(( flags & 0x0F) << 52))     



;[ORG 0x7E00]

[BITS 16]


;section .text

protectedGate1:


;mov bx,0x0000_9000
   
   mov si,String
   call kprint

   cld
   cli

   in al,0x92
   or al,2
   out 0x92,al

   lgdt[gdt32Ptr]
   lidt[IdtPipe]

   mov eax, cr0
   or eax,1
   mov cr0, eax

;   mov [saved_segment],ds

;   jmp code32_post:REBASE_ADDRESS(__protected_mode_32)
   jmp code32_post:__protected_mode_32




;   jmp $
;   jmp code32_post:__protected_mode_32

kprint:
   cld
   mov ah,0x0E
kstring:
   lodsb
   int 0x10
   cmp al,0x00
   jnz kstring
   ret



;section .text

[bits 32]
; 32 bit protected mode
__protected_mode_32:use32
;    mov ax, 0x10
   mov ax,data32_post
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
;    mov ss, ax
    ; restate cr3
    mov cr3, ebx
    ; restate esp
    mov esp, edx

   mov cx,[gate_voucher]
   cmp word [gate_voucher],0
   jnz loopcheck
   mov word [gate_voucher],1

   jmp BIOS32_PREP


loopcheck:
   hlt
   jmp loopcheck

;   jmp 0x8000

BIOS32_PREP:use32


    pusha
    ;pushf
    ; save current esp to edx
    mov edx, esp
    ; jumping to 16 bit protected mode
    ; disable interrupts
    cli
    ; clear cr3 by saving cr3 data in ebx register
    xor ecx, ecx
    mov ebx, cr3
    mov cr3, ecx


   jmp code16_post:__protected_mode_16

[bits 16]
; 16 bit protected mode
__protected_mode_16:use16
    ; jumping to 16 bit real mode

   xor eax,eax
   xor ecx,ecx

;   mov ax, 0x38
   mov cx,data16_post

;    mov ax,0

    mov ds, cx
    mov es, cx
    mov fs, cx
    mov gs, cx
    mov ss, cx
    ; turn off protected mode
    ; set bit 0 to 0
    mov eax, cr0
    and al,  ~0x01
    mov cr0, eax

;   jmp 0x8000

   jmp 0xA000

;section .data

String:   db 'platform 2',10,13,0
checkString:   db 'check',10,13,0


gdt32:
   dq BUILD_GDT_DESC(0,0,0,0)
gdt32code:
   dq BUILD_GDT_DESC(0x0000ffff,0,10011010b,1100b)
gdt32data:
   dq BUILD_GDT_DESC(0x0000ffff,0,10010010b,1100b)
gdt16code:
   dq BUILD_GDT_DESC(0x0000ffff,0,10011010b,1000b)
gdt16data:
   dq BUILD_GDT_DESC(0x0000ffff,0,10010010b,1000b)


.stub1:

   code32_post: equ gdt32code -gdt32
   data32_post: equ gdt32data -gdt32
;.stub:
   code16_post: equ gdt16code -gdt32
   data16_post: equ gdt16data -gdt32

;   tss32_post: equ gdt32tss -gdt32
   
   gdt32Len: equ $-gdt32 
   gdt32Ptr: dw gdt32Len-1
      dd gdt32


save_cr0   dd 0
save_cr3   dd 0
saved_segment   resd 0
gate_voucher   dw 0
saved_stack   resw 0




IdtPipe:
   dw 0x03ff
   dd 0

times 512-($-$$) db 0

;%include 'gdt.inc'



stage2.asm


stage3.asm ( pretty lengthy from the graphic methods and had to attach seperately instead)


stage4.asm (resides as a proof of concept)
Code: [Select]
[org 0x10000]

[BITS 16]

_prep_module2:


_vm_module:
   sub dword [_vidmem_ptr],_VIDEO_TEXT_ADDR
   mov si,_vm_str

   call _printstr_task


_loopcheck: jmp $

_vm_str: db 'long      test 1',0

_printstr_task:
   
   push cx


   mov bp,0b800h
   mov es,bp
   mov bp,[_vidmem_ptr]

_prep:

   mov cx,0
   mov es,cx
   mov ax,_ROWS
   mov bx,es:0x044a
   mul bx
   add ax,_COLS1
;   shl ax,1
   mov bp,ax
   add bp,[_vidmem_ptr]


;check even and odd placements
   
;   wizard tactics 1.7
   add bp,0x690
;   wizard tactics 1.8
;   times 140 inc bp
;   wizard tactics 1.9
   times 20 inc bp
   ;boundary

;check even and odd placements
   
   mov cx,0b800H
   mov es,cx
   mov ch,_COLOR_ATTR_BSC

   jmp _gettext


_output:

   pop ax
   mov dx,ax
   mov es:[bp],dx

   add bp,byte 2


_gettext:

   mov cl,byte [ds:si]
   inc si

   push cx
   test cl,cl
   jnz _output
   mov [_vidmem_ptr],bp

   pop word [bp]
   pop ax

   mov cx,0
   mov ds,cx
   
   ret

   jmp $


_VIDEO_TEXT_ADDR   EQU 0XB8000
_COLOR_ATTR_PSC   EQU 0X6A
_COLOR_ATTR_BSC   EQU 0X5F
_PM_MODE_STACK   EQU 0X80000
_VM_STACK_SEG   EQU 0X0000
_VM_STACK_OFS   EQU 0X0000
_VM_CS_SEG      EQU 0X0000
_FLAGS_VM_CMP   EQU 17
_FLAGS_CMP1   EQU 1
_FLAGS_CMP_IF   EQU 9
_RING0_PROC_STACK_SIZE   EQU 2048
;TSS_IO_MAP_SIZE       EQU 0
_TSS_IO_MAP_SIZE       EQU 0x400/8
;VM_STACK_ADDRESS   EQU vidmem_address
_ROWS   EQU 25
_COLS1   EQU 80
_BACK1   EQU 05h
_FRONT1   EQU 12h
_CHECKER   EQU 0


align 4
_vidmem_ptr: dd _VIDEO_TEXT_ADDR
_pm_str: db 'protected mode string ',0
_pm_str_length: equ $-_pm_str

;vidmem_address: dw 0


times 512-($-$$) db 0


run.sh
Code: [Select]

   nasm -f bin -g -o boot.bin boot.asm

   nasm -f elf -g stage1.asm -o stage1.o
   nasm -f elf -g stage2.asm -o stage2.o
   nasm -f elf -g stage3.asm -o stage3.o
     
   nasm -f bin -g test2.asm -o test2.bin

   ld -g -m elf_i386 -Ttext 0x7e00 stage1.o -o stage1.bin --oformat binary

   ld -g -m elf_i386 -Ttext 0x8000 stage2.o -o stage2.bin --oformat binary

   ld -g -m elf_i386 -Ttext 0xA000 stage3.o -o stage3.bin --oformat binary


   dd if=/dev/zero of=os.bin bs=512 count=2811
   cat boot.bin   \
      stage1.bin   \
      stage2.bin    \
      stage3.bin   \
      test2.bin \
   os.bin>os.img

rm -rf *\.bin


qemu-system-i386 -fda os.img


(1 extra config file)
gdt.inc
Code: [Select]
%define BUILD_GDT_DESC(bounds,base,access,flags) \
((( base & 0x00FFFFFF) << 16) | \
(( base & 0xFF000000) << 32) | \
( bounds & 0x0000FFFF) | \
(( bounds & 0x000F0000) << 32) | \
(( access & 0xFF) << 40) | \
(( flags & 0x0F) << 52))


section .data

gdt32:
dq BUILD_GDT_DESC(0,0,0,0)
gdt32code:
dq BUILD_GDT_DESC(0x0000ffff,0,10011010b,1100b)
gdt32data:
dq BUILD_GDT_DESC(0x0000ffff,0,10010010b,1100b)
gdt16code:
dq BUILD_GDT_DESC(0x0000ffff,0,10011010b,1000b)
gdt16data:
dq BUILD_GDT_DESC(0x0000ffff,0,10010010b,1000b)

;gdt16tss:dq BUILD_GDT_DESC(0x8230,TSS_SIZE-1,10001001b,0000b) & 0 << 22
;gdt32tss:
; dq BUILD_GDT_DESC(TSS_SIZE-1,0x8280,10001001b,0000b)
; dq BUILD_GDT_DESC(TSS_SIZE-1,0x9260,10001001b,0000b)
.stub1:

code32_post: equ gdt32code -gdt32
data32_post: equ gdt32data -gdt32
;.stub:
code16_post: equ gdt16code -gdt32
data16_post: equ gdt16data -gdt32

; tss32_post: equ gdt32tss -gdt32

gdt32Len: equ $-gdt32
gdt32Ptr: dw gdt32Len-1
dd gdt32


IdtPipe:
dw 0x03ff
dd 0

« Last Edit: June 22, 2023, 06:24:21 PM by Deskman243 »

Offline fredericopissarra

  • Full Member
  • **
  • Posts: 373
  • Country: br
Re: How to link ld files segments accordingly for NASM programs
« Reply #1 on: June 22, 2023, 05:05:47 PM »
With legacy master boot record I prefer to encode everything in a single file:
Code: [Select]
; mbr.asm
  bits 16

  ; int 0x19 entry point.
  ; label is here just because of child labels (.???).
_start:
  ; Normalize DS, so out offsets start at 0.
  ; This could be useful if I intended to move these sectors to 0x60:0
  ; at beginning of RAM after Extended BIOS data area. Notice ES sill points
  ; to segment 0. If you need to access BIOS data areas, use ES.
  mov   ax,0x7c0
  mov   ds,ax
  jmp   0x7c0:.begin  ; Normalize CS:(E)IP too.
.begin:

  cld   ; just to make sure direction is up.

  ; Calculate how many extra sectors will be loaded.
  lea  ax,[after_last_sector_offset]
  lea  bx,[next_sector_offset]
  sub  ax,bx
  mov  cl,9
  shr  ax,cl  ; AX = # of sectors to load. (max 127).

  ; Load additional sectors using LBA.
  ; Notice DL is untouched.
  mov  [dap.num_sectors],ax
  mov  [dap.buffer_addr],bx
  mov  [dap.buffer_addr+2],ds
  mov  ah,0x42
  lea  si,[dap]
  int  0x13
  jc   loader_error

  ; We can still do somethings here after loading the
  ; sectors.

  ; jump to code in the next sectors.
  jmp  next_sector_offset

loader_error:
  lea  si,[errmsg]
  call puts
halt:
  hlt
  jmp  halt

; Print a string using TTY.
; Input: DS:SI = ptr to asciiz string.
puts:
  ; push bx     ; preserve BX is I was using an ABI.
  mov bx,7
.loop:
  lodsb
  test al,al
  jz  .exit
  mov ah,0x0e
  int 0x10
  jmp .loop
.exit:
  ; pop bx
  ret

errmsg:
  db  `Error loading sectors from disk.\r\n`,0

  ; Structure used by service 0x42, int 0x13.
dap:
  dw  dap_size
.num_sectors:
  dw  0
.buffer_addr:
  dd  0
  dq  1       ; next sector is 1.
dap_size equ $ - dap

  times 510 - ($ - $$) db 0
  dw  0xaa55    ; end of mbr signature.

; this mark the entry point for code in next sectors.
next_sector_offset:
  mov ax,3
  int 0x10
  lea si,[msg]
  call puts
  jmp  halt

msg:
  db `Never gonna give you up.\r\n`
  db `Never gonna let you down.\r\n`,0

; this label must be aligned by 512 boundary (1 sector)
; to avoid complex (meh!) calculations...
  align 512
after_last_sector_offset:
So, I don't need sections and a linker script.
Code: [Select]
$ nasm -fbin -o mbr.bin mbr.asm
$ qemu-system-i386 -drive file=mbr.bin,index=0,format=raw

« Last Edit: June 22, 2023, 07:20:29 PM by fredericopissarra »

Offline Deskman243

  • Jr. Member
  • *
  • Posts: 49
Re: How to link ld files segments accordingly for NASM programs
« Reply #2 on: June 22, 2023, 08:04:36 PM »
Aww that's really cool. I wish that would run on my code above. I don't think I got the right readings because it won't even boot other than the error text haha . Just to check before where you have the insertion line on the dap configuration for sections I put
Code: [Select]
dap:
  dw  dap_size
.num_sectors:
  dw  0
.buffer_addr:
  dd  0
  dq 0x10000       ; next sector is 1.
dap_size equ $ - dap


I was only trying to make 1 MB for a standard program . Do you know how I could manage that here?
« Last Edit: June 22, 2023, 10:32:38 PM by Deskman243 »

Offline fredericopissarra

  • Full Member
  • **
  • Posts: 373
  • Country: br
Re: How to link ld files segments accordingly for NASM programs
« Reply #3 on: June 22, 2023, 11:30:53 PM »
Aww that's really cool. I wish that would run on my code above. I don't think I got the right readings because it won't even boot other than the error text haha.

Are you using a very, very, old PC? If this is the case, LBA may not be available to your BIOS.

I was only trying to make 1 MB for a standard program . Do you know how I could manage that here?

1 MiB? 2048 sectors? Not possible to read that at 1 read, as it is in documentation you can read, maximum, 64 KiB (if the offset is 0)...

Offline Deskman243

  • Jr. Member
  • *
  • Posts: 49
Re: How to link ld files segments accordingly for NASM programs
« Reply #4 on: June 23, 2023, 02:50:37 PM »
Good news everyone!

Status update for the answer to this article was the ds segment for the graphics component had to be coincidentally referenced. An important distinction is that 0A000h was piped as a test segment for both file address AND segments. This meant simply that only the file address references need changing.

Now I can go back to looking at the 2nd file inference for the a16-19 Data line references that I have been researching in my previous posts. Anyhow I hope this answer serves anyone else too and hope everyone has a good rest of your day. If anyone else would like a more recent version of this program let me know!

Offline Deskman243

  • Jr. Member
  • *
  • Posts: 49
Re: How to link ld files segments accordingly for NASM programs
« Reply #5 on: June 23, 2023, 02:54:10 PM »
Good news everyone!

Status update for the answer to this article was the ds segment for the graphics component had to be coincidentally referenced. An important distinction is that 0A000h was piped as a test segment for both file address AND segments. This meant simply that only the file address references need changing.

Now I can go back to looking at the 2nd file inference for the a16-19 Data line references that I have been researching in my previous posts. Anyhow I hope this answer serves anyone else too and hope everyone has a good rest of your day. If anyone else would like a more recent version of this program let me know!

Quote
1 MiB? 2048 sectors? Not possible to read that at 1 read, as it is in documentation you can read, maximum, 64 KiB (if the offset is 0)...


Offline fredericopissarra

  • Full Member
  • **
  • Posts: 373
  • Country: br
Re: How to link ld files segments accordingly for NASM programs
« Reply #6 on: June 23, 2023, 05:10:06 PM »
Updated code jumping to protected mode. No linker scripts, just NASM.

$ make
$ make run
« Last Edit: June 23, 2023, 05:16:43 PM by fredericopissarra »

Offline fredericopissarra

  • Full Member
  • **
  • Posts: 373
  • Country: br
Re: How to link ld files segments accordingly for NASM programs
« Reply #7 on: June 23, 2023, 06:03:47 PM »
Code: [Select]
dap:
  dw  dap_size
.num_sectors:
  dw  0
.buffer_addr:
  dd  0
  dq 0x10000       ; next sector is 1.
dap_size equ $ - dap

Notice the last DQWORD is the initial sector (numbered from 0 to N). Here you are trying to read sector #65536. What I did earlier was to read sectors following the sector 0 (hence "next sector is 1" comment). If the sector is invalid, service 0x42 will return with Carry set. See Ralf Brown's Interrupt List (google it) for more info.

Take a look at the latest code I posted here...

PS: LBA used in service 0x42 is LBA48 (if supported), then the sectors have a limit of 48 bits. The bigger disk could have 2^48 sectors or 2^57 bytes (128 PiB). If the controller supports only LBA28 then the disk is limited to 2^28 sectors or 2^37 bytes (128 GiB). And, an sector index in LBA always start at 0 (not 1, as in CHS model).
« Last Edit: June 23, 2023, 06:09:03 PM by fredericopissarra »

Offline Deskman243

  • Jr. Member
  • *
  • Posts: 49
Re: How to link ld files segments accordingly for NASM programs
« Reply #8 on: June 24, 2023, 04:34:47 PM »
Good day everyone
Here I got more news about the model version. The funny thing is there's still a bug in the current code that I have found in a specific area. Now that the segments were relatively settled I tried putting more conventional code into the recent model.

Here I think it's very important to state the inferences of these additional code.
First I put a TIMES pad function into the larger graphical function file. The good news is that this actually retains the model as is and therefore happily runs. However the second time I try to test this on the other stage file size this actually changed the path of the program.When I run it on the debugger I was amazed by the difference between the two file instructions.

 I'm mainly referring back to the original access path figure from earlier. From here I basically just change the corresponding amount of sections in boot.asm(Disk Read Functions) from any of the other Stage files. Just to clarify I have a 0x30 (48) configuration for stage3.asm but when I tried changing Stage2.asm from a single 512 section to two 512=1024 segments it suddenly fails.

Quote
Disk_Read(boot.asm)=>Jump_From_Gate(Stage1.asm)=>Jump_Into_BIOS16_Segment(Stage2.asm)=>Stage3_Addressment

Quick check here my build file here is run3.sh and can be run simply by
Code: [Select]
sudo chmod +x run3.sh
sudo ./run3.sh

I'm posting the model for review again to get this model a conclusion once and for all and I'm more than happy to settle this proof of concept. Again cheers and hope y'all have a good one from here!

Revision 2
This was the new test configuration

Disk_Read(boot.asm) => Jump_From_Gate(Stage1.asm)
=>Jump_Into_BIOS16_Segment(Stage2.asm @ 0x8000)
=>test2.asm(data number fill pad @ 0x8200)
=>Stage3_Addressment( @ 0x8400)


Code: [Select]
make clean

nasm -f bin -g -o boot5.bin boot5.asm
nasm -f elf -g -o boot.o boot.asm

nasm -f elf -g stage1.asm -o stage1.o
nasm -f elf -g stage2.asm -o stage2.o
nasm -f elf -g stage3.asm -o stage3.o
nasm -f elf -g stage4.asm -o stage4.o

nasm -f bin -g test2.asm -o test2.bin


ld -T link.ld boot.o -o boot.bin
ld -g -m elf_i386 -Ttext 0x7e00 stage1.o -o stage1.bin --oformat binary
ld -g -m elf_i386 -Ttext 0x8000 stage4.o -o stage4.bin --oformat binary
ld -g -m elf_i386 -Ttext 0x9000 stage2.o -o stage2.bin --oformat binary

dd if=/dev/zero of=ospad1.bin bs=512 count=50

dd if=/dev/zero of=os.bin bs=512 count=2876
cat boot5.bin \
stage1.bin \
stage4.bin \
test2.bin \
os.bin>os.img

rm -rf *\.bin


qemu-system-i386 -fda os.img



« Last Edit: June 24, 2023, 06:38:53 PM by Deskman243 »

Offline Deskman243

  • Jr. Member
  • *
  • Posts: 49
Re: How to link ld files segments accordingly for NASM programs
« Reply #9 on: June 24, 2023, 06:44:36 PM »
GDB check 1

Here where the yields I got from the configuration above from gdb

The evidence shows that the settings duplicate test2.bin @ 0x8200 AND @ 0x8400.
« Last Edit: June 24, 2023, 06:47:07 PM by Deskman243 »