This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

Help me: how to determine constant value at link time?

In ASM51, the following code is every useful:


-------- file1.asm ---------
public MAX
MAX EQU 10

-------- file2.asm ---------
extrn number(MAX)
MOV	A,#MAX

The point is that the constant value of MAX in file2.asm can be determined at link time.

I want the same effect in C51, that's to say, to define a constant as "extern". I have tried a lot of ways. One of them is:

-------- file2.C -----------
extern code MAX;
#define MAX_NUMBER ((unsigned char)&MAX)

C51 is trying to treat MAX_NUMBER as a constant, but the compiled result is much longer than the original one. Is there any better way???

Parents Reply Children
  • The usual way to do this in C would be to supply a header file with some definitions:

    config.h
    extern U8 const myVal;

    and tell users of the library that they need to create an object file that defines values for those constants.

    config.c:
    U8 const myVal = 99;

    You then link config.o with the rest of your library.

    Typically, use of myVal will generate code that loads myVal from some address (determined by the linker) -- something like (at a guess)

    ; small model
    MOV R0,#myVal
    MOV A,@R0

    ; large model
    MOV DPTR,#myVal
    MOVX A,@DPTR

    This code will be larger than code that loads a value as an immediate value. That is, the value is included literally in the instruction, not by reference.

    MOV A,#99

    If this difference is important to you, then life will be a little more painful. When the assembler runs, it actually generates code like this:

    0100: MOV A,#00

    along with an entry in the object file that says "label MAX is at address 101". The linker then replaces the byte at address 101 with the value defined for MAX.

    There is no way to write C code with a "placeholder" value such as the EXTERN NUMBER in the assembler. (Such syntax would have to look something like:

    U16 myMax = ???;

    where "???" is the value to be replaced by the linker. So, there's no way to write C to get that "to be determined" integer.

    There is a way to cheat, though. It's easy to write C code that generates addresses that get patched up by the linker. And you can cast an address. So, you could write C thus:

    extern U8 MAX; // type doesn't really matter
    U16 myMax = (U16)&MAX;

    Now, all you have to do is tell your linker the address of MAX. It will find all references to the address-of-MAX in the object files and fix them up, thus replacing the value in line, as desired.

    I find this technique "painful" because on the one hand, it's not really obvious what you're doing. It obscures the real type and purpose of MAX. And every linker has its own idiosyncratic way of defining symbols, so it's a really non-portable technique. Unless the few extra bytes of code are really going to cripple your application, you might want to just use the first method.

    Yet another method would be to distribute source code (obfuscated, perhaps, if piracy is a concern), put the definitions in a common header file, and have the user recompile the whole works.

  • One year has passed but nobody give me a good answer.

    ... That same year also passed without any feedback from you about the answers you did get. If you didn't consider those "good" answers, why wait all of a whole year to say so?

    Are you actually trying to make us believe it never occured to you that the reason none of the answers were to your liking might have been that the trick you're trying to pull is impossible? Even in the ligth of some of your answers telling you exactly that?

  • Hi,
    I have the following line in my assembly code and I want to link with C program. How to declare in C?

    EXTRN NUMBER (RS485_TASK, RECV_END)

    Thanks in advance.

  • "One year has passed but nobody give me a good answer."

    Well, I'm sorry - I didn't realise I was supposed to drop everything and concentrate on your problem!

    Anyway, think about what the Linker does: it deals with addresses - you define a symbol in 'C', and the Linker fixes its address.
    Therefore, if you want the Linker to give a constant back to 'C', it will be as the address of a 'C' symbol.
    Your 'C' code will have to take the address of the symbol, and interpret that address value as the required constant (probably via a cast).

  • Dear Hans-Bernhard Broeker, and all those who concern my question,

    Actually, I know all the ways everybody have mentioned. And I have tried even more.

    At last I found that the problem lies in the optimization defects of Keil. Let me expain in detail.

    Define in a configuration asm:

    MAX equ 10
    public MAX

    Then declare the variable in C:
    extern unsigned char MAX[1];

    Now lets look at this ideal function:

    uint8 Test(uint8 i)
    {
    	if(i == (uint8)MAX)
    		return 1;
    	return 0;
    }

    and this real one:

    uint8 Test(uint8 i)
    {
    	if(i == (uint8)MAX)
    		return (uint8)MAX;
    	return 0;
    }

    You may find the translated ASM code quite different. Keil consider MAX as constant in the first example, which refers to MAX only once, so CJNE is used to make good (not perfect) ASM code. But in the seconds example, MAX is used twice, so the "register variables" optimization takes effect, which will save i to R7, and then load MAX to R7, so as to use MAX in the return statement.

    As a result, the second function will use one more register and the ASM code size is doubled! My project is an Operation system, which requires that most functions be concise and reentrantable. Unfortunately, when this trick is used, my OS functions fail to be reentrantable! I don't want to use full ASM as this OS can be transplanted to other CPU core.

    I really need this link-time constant determination, because I don't want to share my source code. Currently, my solution is to use
    extern code uint8 MAX
    and then define the value of MAX in an configuration ASM file. In this way, the "register variables" optimization is less effective and produce acceptable code.

    So, thanks to you all, and I wish Keil will do better in the future version. But I don't know whether that's hopeful.

  • You don't give any real explanation why the differences in behaviour of the code for these two particular functions would constitute an optimizer defect.

    C51 functions aren't reentrant unless you ask for them to be. That has nothing to do with the subject of this thread.

  • "At last I found that the problem lies in the optimization defects of Keil."

    Or perhaps in your understanding of the 'C' programming language?

    "You may find the translated ASM code quite different."

    Well of course you will - the two source functions are quite different:
    The "ideal" function only has to choose one of the two manifest constants 0 or 1; the "real" function may have to return the actual value of the symbol MAX

    "Keil consider MAX as constant in the first example"

    That's because it is a constant: you defined MAX as an array; in 'C', using the array name on its own like this (with no index) is effectively a constant pointer:

    "MAX" is equivalent to "&MAX[0]"

    "But in the seconds example, MAX is used twice"

    Of course it is - because you used it twice in the source code!
    First, in the comparison operation;
    Second, in the return operation

    "so the 'register variables' optimization takes effect, which will save i to R7, and then load MAX to R7, so as to use MAX in the return statement."

    What else could it do?
    Look at the C51 calling convention in the Manual - that defines what registers must be used for passing parameters & return values.

    "As a result, the second function will use one more register and the ASM code size is doubled!"

    I make it 11 bytes for the 1st function, and 12 for the second - methinks thou dost protest too much!

                 ; FUNCTION _idealTest (BEGIN)
                                               ; SOURCE LINE # 6
    ;---- Variable 'i' assigned to Register 'R7' ----
                                               ; SOURCE LINE # 7
                                               ; SOURCE LINE # 8
    0000 7400        E     MOV     A,#LOW MAX
    0002 B50703            CJNE    A,AR7,?C0001
                                               ; SOURCE LINE # 9
    0005 7F01              MOV     R7,#01H
    0007 22                RET
    0008         ?C0001:
                                               ; SOURCE LINE # 10
    0008 7F00              MOV     R7,#00H
                                               ; SOURCE LINE # 11
    000A         ?C0002:
    000A 22                RET
                 ; FUNCTION _idealTest (END)
                 ; FUNCTION _realTest (BEGIN)
                                               ; SOURCE LINE # 14
    ;---- Variable 'i' assigned to Register 'R6' ----
    0000 AE07              MOV     R6,AR7
                                               ; SOURCE LINE # 15
                                               ; SOURCE LINE # 16
    0002 7F00        E     MOV     R7,#LOW MAX
    0004 EF                MOV     A,R7
    0005 B50601            CJNE    A,AR6,?C0003
                                               ; SOURCE LINE # 17
    0008 22                RET
    0009         ?C0003:
                                               ; SOURCE LINE # 18
    0009 7F00              MOV     R7,#00H
                                               ; SOURCE LINE # 19
    000B         ?C0004:
    000B 22                RET
                 ; FUNCTION _realTest (END)

    If you're desperate to reduce the size of the function, you might like to try this:
    uint8 goodTest( const uint8 testmax, uint8 i)
    {
    	if(i == (uint8)testmax)
    		return (uint8)testmax;
    	return 0;
    }
    Which uses only 8 bytes of assembler - because I've been careful to force MAX to be in the right register to start with:
                 ; FUNCTION _goodTest (BEGIN)
                                               ; SOURCE LINE # 21
    ;---- Variable 'testmax' assigned to Register 'R7' ----
    ;---- Variable 'i' assigned to Register 'R5' ----
                                               ; SOURCE LINE # 22
                                               ; SOURCE LINE # 23
    0000 ED                MOV     A,R5
    0001 B50701            CJNE    A,AR7,?C0005
                                               ; SOURCE LINE # 24
    0004 22                RET
    0005         ?C0005:
                                               ; SOURCE LINE # 25
    0005 7F00              MOV     R7,#00H
                                               ; SOURCE LINE # 26
    0007         ?C0006:
    0007 22                RET
                 ; FUNCTION _goodTest (END)
    but it (probably) increases the call overhead! :-(
    But the call overhead will always be your downfall when implementing trivial operations as 'C' functions!

    "My project is an Operation system, which requires that most functions be concise and reentrantable."

    But C51 is inherently non-reentrant - as clearly stated in the Manual!

  • I think your explaination is quite good and everything is OK. Unfortunately, you did not resolve the problem that: ONE MORE REGISTER IS USED AND MAKE THE FUNCTION UN-REENTRANTABLE, because in my project, the function is quite complexed and one more abuse of register will make the function un-reentrantable. If you replace MAX with a constant value such as

    #define MAX 5

    then the extra register will not be used and make the function reentrantable.

    Please note that my problem is to pass MAX as a constant whose value is defined in an ASM configuration file. In your example of passing it as a funtion parameter, it's meaningless to me.

    By they way, a function is inherently reentrantable if it only uses registers as local automatic variables. The "reentrant" keywork is using a software stack to realize reentrant feature. If you are not clear about this, you will hardly understant my question. For example, such functions as "memcpy", "isdigit" are reentrant (please refer to the manual), but they don't use software stack at all.

    Now. Please see the result of my "optimization defect" example:

    Example 1:

    #define MAX 5
    uint8 Test(uint8 i)
    {
    	if(i == (uint8)MAX)
    		return (uint8)MAX;
    	return 0;
    }
    The ASM code is:
    0000 BF0503            CJNE    R7,#05H,?C0140
    0003 7F05              MOV     R7,#05H
    0005 22                RET
    0006         ?C0140:
    0006 7F00              MOV     R7,#00H
    0008 22                RET     
    It has 3 instrunctions excluding "RET"

    Example 2:
    Just replace the define statement to:
    extern uint8 MAX[1];
    now the ASM code is:
    0000 AE07              MOV     R6,AR7
    0002 7F00        E     MOV     R7,#LOW MAX
    0004 EF                MOV     A,R7
    0005 B50601            CJNE    A,AR6,?C0140
    0008 22                RET
    0009         ?C0140:
    0009 7F00              MOV     R7,#00H
    000B 22                RET     
    It has 5 instrunctions excluding "RET"

    We exclude "RET" from calculation since in our real project, the function is quiet complex and the size grows larger, while the "RET" statment remains the same number of occurrence.

    Example 1, good result, nothing to blame.
    In example 2, Keil treat MAX as a variable and try to save it in a cache, which is R7, for further operation, this is what the "register variables" does.

    As a result, in the real project, if we use "extern uint8 MAX[1]" method to pass configuration constant, the function will become larger, exhaust all registers and make the function non-reentrantable.