8

I am trying to recreate the C source code from some 16bit DOS 8086 assembly generated by the MS C 5.0 compiler. I've hit a wall with this far call instruction.

0000008D  9A2F0CB506        call 0x6b5:0xc2f

The MZ executable has a relocation entry on the 0x6b5 value which is actually the data segment. Looking at the data around the 0xc2f offset in IDA, this looks like an array of trampoline-like function stubs consisting of a 0xea far jump opcode followed by a null segment and offset that are patched at runtime:

dataSeg:0C2F sub_0C2F proc far
dataSeg:0C2F                 jmp     far ptr loc_5C30
dataSeg:0C2F sub_0C2F endp
dataSeg:0C34 sub_0C34 proc far
dataSeg:0C34                 jmp     far ptr loc_5C35
dataSeg:0C34 sub_0C34 endp
dataSeg:0C39 sub_0C39 proc far
dataSeg:0C39                 jmp     far ptr loc_5C3A
dataSeg:0C39 sub_0C39 endp

Now I'm trying to write C code that would generate the identical assembly:

uint8 test[] = { 0xea, 0x12, 0x34, 0x56, 0x78 };
typedef void(far *func)(void);
int main(void) {
    // cast data segment address to a function pointer and call
    ((func)test)();
}

However, the compiler generates this instead:

00000080  B84200            mov ax,0x42 ; the offset of 'test' in the data segment
00000083  8CDA              mov dx,ds
00000085  8956F0            mov [bp-0x10],dx ; put segment and offset on the stack
00000088  8946EE            mov [bp-0x12],ax
0000008B  FF5EEE            call far [bp-0x12] ; call through temporary pointer placed on the stack

I've tried using a different memory model (medium/compact), changed the array declaration to static and far, still no luck.

One thing I noticed (thanks user @the busybee) was that when using the medium model (multiple code segments), functions from a different translation unit (.c file) are placed in a different code segment, and called through a literal segment:offset far pointer call like I need, but there doesn't seem to be a way to force those functions into the data segment, or conform to the 5-byte layout (jmp opcode + 4 bytes address) from C.

Another thing I tried next was to put the uint8 buffer in a different translation unit, but an attempt to cast its address to a far function pointer still results in a call via a temporary far pointer on the stack.

What other C code could potentially generate a far call into the data segment with an immediate relocated segment and offset like the original?

17
  • 1
    @user253751: if it was declared as a function, then the compiler would place it in the code segment. Meanwhile, it's surrounded by data on both sides. Also, the exact value of DS is unknown at runtime, but the offset of the data segment (here, 0x6b5) in the exe is known, and is relocated by DOS to the correct value when loaded, by patching the immediate values in the instructions. Commented Mar 17, 2023 at 8:29
  • 5
    Since the call's target is in a jump table, the executable might have been linked from multiple modules. That jump table could be created by something else but a C compiler. -- Another thought: Did you check how a far call to a function in another module is generated, in a memory model that supports multiple code segments? Commented Mar 17, 2023 at 10:44
  • 1
    @user253751: This is DOS, so there is no memory protection or segment attributes, code can be used as data, and data can be executed as code. This compiler is quite primitive, it lays out the data and code in the same order they are specified in the source. If the jump table is in the middle of a data segment, that means it was defined in the C source as data, between other data objects. It is not a matter of what I "want", the code that I'm trying to recreate is obviously executing contents of the data segment as code. Commented Mar 17, 2023 at 12:09
  • 6
    Have you tried to create an EXE file from two C files, using extern void far test(void); in one C file and uint8 test[] = { ... }; in the other C file? Using Microsoft Quick C for DOS, this approach worked. Commented Mar 20, 2023 at 10:58
  • 2
    @MartinRosenau: That's brilliant, it worked! I never thought to define the symbol as one thing and declare it as another in the header, without an intermediate cast. I started working on writing the buffer contents in assembly as far proc-s, inside a segment that I would make a part of DGROUP, but your approach makes that unnecessary. Would you care to write it up as an answer so I can accept it? Thank you. Commented Mar 21, 2023 at 12:08

1 Answer 1

5

The MZ executable has a relocation entry on the 0x6b5 value which is actually the data segment. Looking at the data around the 0xc2f offset in IDA, this looks like an array of trampoline-like function stubs ...

I assume that these functions are originally not written in C, but in assembly code. (Not inline assembly but a separate assembly file).

The C file looks like this:

extern void far test(void);
...
test();

The assembly file looks like this:

.data
.globl _test
_test:
    jmp far xyz

This means that the instruction "call 0x6b5:0xc2f" (generated by C code) and the trampoline functions in the data section are located in different object files (one object file generated by the C compiler and one object file generated by the assembler).

What other C code could potentially generate a far call into the data segment with an immediate relocated segment and offset like the original?

You can achieve the same result without using a C compiler only (not using an assembler) by placing the trampoline functions in a separate C file so two different object files are generated.

The (first) C file remains unchanged: It contains the declaration extern void far test(void);

The second C file replaces the assembly file and it looks like this:

uint8 test[] =
{
    ...
};

Note ...

... that the Microsoft C compiler adds an underscore _ at the start of variable names and _cdecl functions internally.

So the function test() and the variable test are named _test in the object file.

For other calling conventions, C compilers handle this differently. As an example, the Watcom default calling convention appends the underscore, so the function is named test_ in the object file. However, for variable names, the underscore is also prepended (_test) in this case.

In this case, you'll have to name the function void _test(void) and the variable uint8_t test_[] to ensure that both have the same name (_test_) in the object file.

2
  • Thanks; using just C worked for the case of one function, but when I tried to add more, the compiler inserted padding between the individual arrays, despite the docs stating char arrays were byte-aligned. When added as one big array, pointer arithmetic on the far function pointer to get to the specific function surprisingly resulted in the same cal address every time. Then I gave up and went with assembly, detailed writeup at neuviemeporte.github.io/f15-se2/2023/03/23/farcalls.html Commented Mar 26, 2023 at 20:19
  • @neuviemeporte As I already wrote, I assume that the trampoline functions are originally not written in C but in assembly language. In assembly language, you have control over the padding bytes... Commented Mar 28, 2023 at 5:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .